claudaborative-editing 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.
- package/LICENSE +335 -0
- package/README.md +106 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2414 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2414 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/yjs/types.ts
|
|
13
|
+
function getDefaultAttributes(blockName) {
|
|
14
|
+
return DEFAULT_BLOCK_ATTRIBUTES[blockName] ?? {};
|
|
15
|
+
}
|
|
16
|
+
function isRichTextAttribute(blockName, attributeName) {
|
|
17
|
+
return RICH_TEXT_ATTRIBUTES[blockName]?.has(attributeName) ?? false;
|
|
18
|
+
}
|
|
19
|
+
var CRDT_RECORD_MAP_KEY, CRDT_STATE_MAP_KEY, CRDT_STATE_MAP_VERSION_KEY, CRDT_STATE_MAP_SAVED_AT_KEY, CRDT_STATE_MAP_SAVED_BY_KEY, CRDT_DOC_VERSION, RICH_TEXT_ATTRIBUTES, DEFAULT_BLOCK_ATTRIBUTES;
|
|
20
|
+
var init_types = __esm({
|
|
21
|
+
"src/yjs/types.ts"() {
|
|
22
|
+
"use strict";
|
|
23
|
+
CRDT_RECORD_MAP_KEY = "document";
|
|
24
|
+
CRDT_STATE_MAP_KEY = "state";
|
|
25
|
+
CRDT_STATE_MAP_VERSION_KEY = "version";
|
|
26
|
+
CRDT_STATE_MAP_SAVED_AT_KEY = "savedAt";
|
|
27
|
+
CRDT_STATE_MAP_SAVED_BY_KEY = "savedBy";
|
|
28
|
+
CRDT_DOC_VERSION = 1;
|
|
29
|
+
RICH_TEXT_ATTRIBUTES = {
|
|
30
|
+
"core/paragraph": /* @__PURE__ */ new Set(["content"]),
|
|
31
|
+
"core/heading": /* @__PURE__ */ new Set(["content"]),
|
|
32
|
+
"core/list-item": /* @__PURE__ */ new Set(["content"]),
|
|
33
|
+
"core/quote": /* @__PURE__ */ new Set(["value", "citation"]),
|
|
34
|
+
"core/pullquote": /* @__PURE__ */ new Set(["value", "citation"]),
|
|
35
|
+
"core/verse": /* @__PURE__ */ new Set(["content"]),
|
|
36
|
+
"core/preformatted": /* @__PURE__ */ new Set(["content"]),
|
|
37
|
+
"core/freeform": /* @__PURE__ */ new Set(["content"]),
|
|
38
|
+
"core/button": /* @__PURE__ */ new Set(["text"]),
|
|
39
|
+
"core/table": /* @__PURE__ */ new Set([]),
|
|
40
|
+
// table cells use rich-text but are nested in body array
|
|
41
|
+
"core/footnotes": /* @__PURE__ */ new Set(["content"])
|
|
42
|
+
};
|
|
43
|
+
DEFAULT_BLOCK_ATTRIBUTES = {
|
|
44
|
+
"core/paragraph": { dropCap: false },
|
|
45
|
+
"core/heading": { level: 2 },
|
|
46
|
+
"core/list": { ordered: false }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// src/yjs/block-converter.ts
|
|
52
|
+
import * as Y from "yjs";
|
|
53
|
+
function blockToYMap(block) {
|
|
54
|
+
const ymap = new Y.Map();
|
|
55
|
+
ymap.set("name", block.name);
|
|
56
|
+
ymap.set("clientId", block.clientId);
|
|
57
|
+
if (block.isValid !== void 0) {
|
|
58
|
+
ymap.set("isValid", block.isValid);
|
|
59
|
+
}
|
|
60
|
+
if (block.originalContent !== void 0) {
|
|
61
|
+
ymap.set("originalContent", block.originalContent);
|
|
62
|
+
}
|
|
63
|
+
const attrMap = new Y.Map();
|
|
64
|
+
if (block.attributes) {
|
|
65
|
+
for (const [key, value] of Object.entries(block.attributes)) {
|
|
66
|
+
if (isRichTextAttribute(block.name, key) && typeof value === "string") {
|
|
67
|
+
const ytext = new Y.Text();
|
|
68
|
+
ytext.insert(0, value);
|
|
69
|
+
attrMap.set(key, ytext);
|
|
70
|
+
} else {
|
|
71
|
+
attrMap.set(key, value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
ymap.set("attributes", attrMap);
|
|
76
|
+
const innerBlocksArray = new Y.Array();
|
|
77
|
+
if (block.innerBlocks && block.innerBlocks.length > 0) {
|
|
78
|
+
const innerMaps = block.innerBlocks.map((inner) => blockToYMap(inner));
|
|
79
|
+
innerBlocksArray.push(innerMaps);
|
|
80
|
+
}
|
|
81
|
+
ymap.set("innerBlocks", innerBlocksArray);
|
|
82
|
+
return ymap;
|
|
83
|
+
}
|
|
84
|
+
function yMapToBlock(ymap) {
|
|
85
|
+
const name = ymap.get("name");
|
|
86
|
+
const clientId = ymap.get("clientId");
|
|
87
|
+
const attrMap = ymap.get("attributes");
|
|
88
|
+
const attributes = {};
|
|
89
|
+
if (attrMap) {
|
|
90
|
+
for (const [key, value] of attrMap.entries()) {
|
|
91
|
+
if (value instanceof Y.Text) {
|
|
92
|
+
attributes[key] = value.toString();
|
|
93
|
+
} else {
|
|
94
|
+
attributes[key] = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const innerBlocksArray = ymap.get("innerBlocks");
|
|
99
|
+
const innerBlocks = [];
|
|
100
|
+
if (innerBlocksArray) {
|
|
101
|
+
for (let i = 0; i < innerBlocksArray.length; i++) {
|
|
102
|
+
innerBlocks.push(yMapToBlock(innerBlocksArray.get(i)));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const block = {
|
|
106
|
+
name,
|
|
107
|
+
clientId,
|
|
108
|
+
attributes,
|
|
109
|
+
innerBlocks
|
|
110
|
+
};
|
|
111
|
+
const isValid = ymap.get("isValid");
|
|
112
|
+
if (isValid !== void 0) {
|
|
113
|
+
block.isValid = isValid;
|
|
114
|
+
}
|
|
115
|
+
const originalContent = ymap.get("originalContent");
|
|
116
|
+
if (originalContent !== void 0) {
|
|
117
|
+
block.originalContent = originalContent;
|
|
118
|
+
}
|
|
119
|
+
return block;
|
|
120
|
+
}
|
|
121
|
+
function computeTextDelta(oldValue, newValue) {
|
|
122
|
+
if (oldValue === newValue) return null;
|
|
123
|
+
let prefixLen = 0;
|
|
124
|
+
while (prefixLen < oldValue.length && prefixLen < newValue.length && oldValue[prefixLen] === newValue[prefixLen]) {
|
|
125
|
+
prefixLen++;
|
|
126
|
+
}
|
|
127
|
+
let suffixLen = 0;
|
|
128
|
+
while (suffixLen < oldValue.length - prefixLen && suffixLen < newValue.length - prefixLen && oldValue[oldValue.length - 1 - suffixLen] === newValue[newValue.length - 1 - suffixLen]) {
|
|
129
|
+
suffixLen++;
|
|
130
|
+
}
|
|
131
|
+
const deleteCount = oldValue.length - prefixLen - suffixLen;
|
|
132
|
+
const insertText = newValue.slice(prefixLen, newValue.length - suffixLen);
|
|
133
|
+
return { prefixLen, deleteCount, insertText };
|
|
134
|
+
}
|
|
135
|
+
function deltaUpdateYText(ytext, newValue) {
|
|
136
|
+
const oldValue = ytext.toString();
|
|
137
|
+
const delta = computeTextDelta(oldValue, newValue);
|
|
138
|
+
if (!delta) return;
|
|
139
|
+
const ops = [];
|
|
140
|
+
if (delta.prefixLen > 0) ops.push({ retain: delta.prefixLen });
|
|
141
|
+
if (delta.deleteCount > 0) ops.push({ delete: delta.deleteCount });
|
|
142
|
+
if (delta.insertText.length > 0) ops.push({ insert: delta.insertText });
|
|
143
|
+
if (ops.length > 0) {
|
|
144
|
+
ytext.applyDelta(ops);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function findHtmlSafeChunkEnd(text, offset, preferredSize) {
|
|
148
|
+
const end = Math.min(offset + preferredSize, text.length);
|
|
149
|
+
if (end >= text.length) return text.length;
|
|
150
|
+
let inTag = false;
|
|
151
|
+
for (let i = end - 1; i >= offset; i--) {
|
|
152
|
+
if (text[i] === ">") {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (text[i] === "<") {
|
|
156
|
+
inTag = true;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!inTag) return end;
|
|
161
|
+
const closingBracket = text.indexOf(">", end);
|
|
162
|
+
if (closingBracket === -1) {
|
|
163
|
+
return text.length;
|
|
164
|
+
}
|
|
165
|
+
return closingBracket + 1;
|
|
166
|
+
}
|
|
167
|
+
var init_block_converter = __esm({
|
|
168
|
+
"src/yjs/block-converter.ts"() {
|
|
169
|
+
"use strict";
|
|
170
|
+
init_types();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// src/yjs/document-manager.ts
|
|
175
|
+
import * as Y2 from "yjs";
|
|
176
|
+
var DocumentManager;
|
|
177
|
+
var init_document_manager = __esm({
|
|
178
|
+
"src/yjs/document-manager.ts"() {
|
|
179
|
+
"use strict";
|
|
180
|
+
init_types();
|
|
181
|
+
init_block_converter();
|
|
182
|
+
DocumentManager = class {
|
|
183
|
+
/**
|
|
184
|
+
* Create a new Y.Doc initialized with Gutenberg's expected structure.
|
|
185
|
+
*/
|
|
186
|
+
createDoc() {
|
|
187
|
+
const doc = new Y2.Doc();
|
|
188
|
+
doc.transact(() => {
|
|
189
|
+
const stateMap = doc.getMap(CRDT_STATE_MAP_KEY);
|
|
190
|
+
stateMap.set(CRDT_STATE_MAP_VERSION_KEY, CRDT_DOC_VERSION);
|
|
191
|
+
});
|
|
192
|
+
return doc;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get the root 'document' Y.Map.
|
|
196
|
+
*/
|
|
197
|
+
getDocumentMap(doc) {
|
|
198
|
+
return doc.getMap(CRDT_RECORD_MAP_KEY);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get the root 'state' Y.Map.
|
|
202
|
+
*/
|
|
203
|
+
getStateMap(doc) {
|
|
204
|
+
return doc.getMap(CRDT_STATE_MAP_KEY);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Read the title as a plain string.
|
|
208
|
+
*/
|
|
209
|
+
getTitle(doc) {
|
|
210
|
+
const documentMap = this.getDocumentMap(doc);
|
|
211
|
+
const title = documentMap.get("title");
|
|
212
|
+
if (title instanceof Y2.Text) {
|
|
213
|
+
return title.toString();
|
|
214
|
+
}
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Set the title (replaces full Y.Text content).
|
|
219
|
+
*/
|
|
220
|
+
setTitle(doc, title) {
|
|
221
|
+
doc.transact(() => {
|
|
222
|
+
const documentMap = this.getDocumentMap(doc);
|
|
223
|
+
const ytext = documentMap.get("title");
|
|
224
|
+
if (ytext instanceof Y2.Text) {
|
|
225
|
+
deltaUpdateYText(ytext, title);
|
|
226
|
+
} else {
|
|
227
|
+
const newYText = new Y2.Text();
|
|
228
|
+
newYText.insert(0, title);
|
|
229
|
+
documentMap.set("title", newYText);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get all blocks as plain Block[] objects.
|
|
235
|
+
*/
|
|
236
|
+
getBlocks(doc) {
|
|
237
|
+
const documentMap = this.getDocumentMap(doc);
|
|
238
|
+
const blocksArray = documentMap.get("blocks");
|
|
239
|
+
if (!blocksArray) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const blocks = [];
|
|
243
|
+
for (let i = 0; i < blocksArray.length; i++) {
|
|
244
|
+
blocks.push(yMapToBlock(blocksArray.get(i)));
|
|
245
|
+
}
|
|
246
|
+
return blocks;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Set blocks from plain Block[] objects (replaces all existing blocks).
|
|
250
|
+
*/
|
|
251
|
+
setBlocks(doc, blocks) {
|
|
252
|
+
doc.transact(() => {
|
|
253
|
+
const documentMap = this.getDocumentMap(doc);
|
|
254
|
+
let blocksArray = documentMap.get("blocks");
|
|
255
|
+
if (!blocksArray) {
|
|
256
|
+
blocksArray = new Y2.Array();
|
|
257
|
+
documentMap.set("blocks", blocksArray);
|
|
258
|
+
}
|
|
259
|
+
if (blocksArray.length > 0) {
|
|
260
|
+
blocksArray.delete(0, blocksArray.length);
|
|
261
|
+
}
|
|
262
|
+
const ymaps = blocks.map((block) => blockToYMap(block));
|
|
263
|
+
blocksArray.push(ymaps);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get a single block by index. Supports dot notation for nested blocks
|
|
268
|
+
* (e.g., "2.1" means inner block at index 1 of top-level block at index 2).
|
|
269
|
+
*/
|
|
270
|
+
getBlockByIndex(doc, index) {
|
|
271
|
+
const ymap = this._resolveBlockYMap(doc, index);
|
|
272
|
+
if (!ymap) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return yMapToBlock(ymap);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Update a block's content and/or attributes at a given index.
|
|
279
|
+
*/
|
|
280
|
+
updateBlock(doc, index, changes) {
|
|
281
|
+
doc.transact(() => {
|
|
282
|
+
const ymap = this._resolveBlockYMap(doc, index);
|
|
283
|
+
if (!ymap) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const blockName = ymap.get("name");
|
|
287
|
+
const attrMap = ymap.get("attributes");
|
|
288
|
+
if (changes.content !== void 0) {
|
|
289
|
+
if (isRichTextAttribute(blockName, "content")) {
|
|
290
|
+
const ytext = attrMap.get("content");
|
|
291
|
+
if (ytext instanceof Y2.Text) {
|
|
292
|
+
deltaUpdateYText(ytext, changes.content);
|
|
293
|
+
} else {
|
|
294
|
+
const newYText = new Y2.Text();
|
|
295
|
+
newYText.insert(0, changes.content);
|
|
296
|
+
attrMap.set("content", newYText);
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
attrMap.set("content", changes.content);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (changes.attributes) {
|
|
303
|
+
for (const [key, value] of Object.entries(changes.attributes)) {
|
|
304
|
+
if (isRichTextAttribute(blockName, key) && typeof value === "string") {
|
|
305
|
+
const existing = attrMap.get(key);
|
|
306
|
+
if (existing instanceof Y2.Text) {
|
|
307
|
+
deltaUpdateYText(existing, value);
|
|
308
|
+
} else {
|
|
309
|
+
const newYText = new Y2.Text();
|
|
310
|
+
newYText.insert(0, value);
|
|
311
|
+
attrMap.set(key, newYText);
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
attrMap.set(key, value);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Insert a block at the given position in the top-level blocks array.
|
|
322
|
+
*/
|
|
323
|
+
insertBlock(doc, position, block) {
|
|
324
|
+
doc.transact(() => {
|
|
325
|
+
const documentMap = this.getDocumentMap(doc);
|
|
326
|
+
let blocksArray = documentMap.get("blocks");
|
|
327
|
+
if (!blocksArray) {
|
|
328
|
+
blocksArray = new Y2.Array();
|
|
329
|
+
documentMap.set("blocks", blocksArray);
|
|
330
|
+
}
|
|
331
|
+
const ymap = blockToYMap(block);
|
|
332
|
+
blocksArray.insert(position, [ymap]);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Remove `count` blocks starting at `startIndex`.
|
|
337
|
+
*/
|
|
338
|
+
removeBlocks(doc, startIndex, count) {
|
|
339
|
+
doc.transact(() => {
|
|
340
|
+
const documentMap = this.getDocumentMap(doc);
|
|
341
|
+
const blocksArray = documentMap.get("blocks");
|
|
342
|
+
blocksArray.delete(startIndex, count);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Insert a block as an inner block of a parent block.
|
|
347
|
+
*/
|
|
348
|
+
insertInnerBlock(doc, parentIndex, position, block) {
|
|
349
|
+
doc.transact(() => {
|
|
350
|
+
const parentYMap = this._resolveBlockYMap(doc, parentIndex);
|
|
351
|
+
if (!parentYMap) {
|
|
352
|
+
throw new Error(`Block not found at index ${parentIndex}`);
|
|
353
|
+
}
|
|
354
|
+
let innerBlocksArray = parentYMap.get("innerBlocks");
|
|
355
|
+
if (!innerBlocksArray) {
|
|
356
|
+
innerBlocksArray = new Y2.Array();
|
|
357
|
+
parentYMap.set("innerBlocks", innerBlocksArray);
|
|
358
|
+
}
|
|
359
|
+
const ymap = blockToYMap(block);
|
|
360
|
+
innerBlocksArray.insert(position, [ymap]);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Remove inner blocks from a parent block.
|
|
365
|
+
*/
|
|
366
|
+
removeInnerBlocks(doc, parentIndex, startIndex, count) {
|
|
367
|
+
doc.transact(() => {
|
|
368
|
+
const parentYMap = this._resolveBlockYMap(doc, parentIndex);
|
|
369
|
+
if (!parentYMap) {
|
|
370
|
+
throw new Error(`Block not found at index ${parentIndex}`);
|
|
371
|
+
}
|
|
372
|
+
const innerBlocksArray = parentYMap.get("innerBlocks");
|
|
373
|
+
if (!innerBlocksArray) {
|
|
374
|
+
throw new Error(`Block at ${parentIndex} has no inner blocks`);
|
|
375
|
+
}
|
|
376
|
+
innerBlocksArray.delete(startIndex, count);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Move a block from one position to another.
|
|
381
|
+
*/
|
|
382
|
+
moveBlock(doc, fromIndex, toIndex) {
|
|
383
|
+
doc.transact(() => {
|
|
384
|
+
const documentMap = this.getDocumentMap(doc);
|
|
385
|
+
const blocksArray = documentMap.get("blocks");
|
|
386
|
+
const block = yMapToBlock(blocksArray.get(fromIndex));
|
|
387
|
+
blocksArray.delete(fromIndex, 1);
|
|
388
|
+
const adjustedIndex = fromIndex < toIndex ? toIndex - 1 : toIndex;
|
|
389
|
+
const ymap = blockToYMap(block);
|
|
390
|
+
blocksArray.insert(adjustedIndex, [ymap]);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Mark the document as saved by updating the state map.
|
|
395
|
+
*/
|
|
396
|
+
markSaved(doc) {
|
|
397
|
+
doc.transact(() => {
|
|
398
|
+
const stateMap = this.getStateMap(doc);
|
|
399
|
+
stateMap.set(CRDT_STATE_MAP_SAVED_AT_KEY, Date.now());
|
|
400
|
+
stateMap.set(CRDT_STATE_MAP_SAVED_BY_KEY, doc.clientID);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get the content field as a string.
|
|
405
|
+
*/
|
|
406
|
+
getContent(doc) {
|
|
407
|
+
const documentMap = this.getDocumentMap(doc);
|
|
408
|
+
const content = documentMap.get("content");
|
|
409
|
+
if (content instanceof Y2.Text) {
|
|
410
|
+
return content.toString();
|
|
411
|
+
}
|
|
412
|
+
return "";
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Set the content field.
|
|
416
|
+
*/
|
|
417
|
+
setContent(doc, content) {
|
|
418
|
+
doc.transact(() => {
|
|
419
|
+
const documentMap = this.getDocumentMap(doc);
|
|
420
|
+
const ytext = documentMap.get("content");
|
|
421
|
+
if (ytext instanceof Y2.Text) {
|
|
422
|
+
deltaUpdateYText(ytext, content);
|
|
423
|
+
} else {
|
|
424
|
+
const newYText = new Y2.Text();
|
|
425
|
+
newYText.insert(0, content);
|
|
426
|
+
documentMap.set("content", newYText);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get a post property from the document map.
|
|
432
|
+
*/
|
|
433
|
+
getProperty(doc, key) {
|
|
434
|
+
const documentMap = this.getDocumentMap(doc);
|
|
435
|
+
const value = documentMap.get(key);
|
|
436
|
+
if (value instanceof Y2.Text) {
|
|
437
|
+
return value.toString();
|
|
438
|
+
}
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Set a post property in the document map.
|
|
443
|
+
*/
|
|
444
|
+
setProperty(doc, key, value) {
|
|
445
|
+
doc.transact(() => {
|
|
446
|
+
const documentMap = this.getDocumentMap(doc);
|
|
447
|
+
const existing = documentMap.get(key);
|
|
448
|
+
if (existing instanceof Y2.Text && typeof value === "string") {
|
|
449
|
+
deltaUpdateYText(existing, value);
|
|
450
|
+
} else {
|
|
451
|
+
documentMap.set(key, value);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get the raw Y.Text for a block attribute.
|
|
457
|
+
* Returns null if the block doesn't exist, the attribute doesn't exist,
|
|
458
|
+
* or the attribute is not a Y.Text (i.e., not a rich-text attribute).
|
|
459
|
+
*/
|
|
460
|
+
getBlockAttributeYText(doc, index, attrName) {
|
|
461
|
+
const ymap = this._resolveBlockYMap(doc, index);
|
|
462
|
+
if (!ymap) return null;
|
|
463
|
+
const attrMap = ymap.get("attributes");
|
|
464
|
+
if (!attrMap) return null;
|
|
465
|
+
const attr = attrMap.get(attrName);
|
|
466
|
+
return attr instanceof Y2.Text ? attr : null;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Get the raw Y.Text for a block's content attribute.
|
|
470
|
+
* Returns null if the block or content attribute doesn't exist.
|
|
471
|
+
*/
|
|
472
|
+
getBlockContentYText(doc, index) {
|
|
473
|
+
return this.getBlockAttributeYText(doc, index, "content");
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Resolve a dot-notation index to a Y.Map block reference.
|
|
477
|
+
* E.g., "2" → top-level block 2, "2.1" → inner block 1 of block 2.
|
|
478
|
+
*/
|
|
479
|
+
_resolveBlockYMap(doc, index) {
|
|
480
|
+
const parts = index.split(".").map(Number);
|
|
481
|
+
const documentMap = this.getDocumentMap(doc);
|
|
482
|
+
const blocksArray = documentMap.get("blocks");
|
|
483
|
+
if (parts.length === 0 || isNaN(parts[0])) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
let current = null;
|
|
487
|
+
let currentArray = blocksArray;
|
|
488
|
+
for (const part of parts) {
|
|
489
|
+
if (part < 0 || part >= currentArray.length) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
current = currentArray.get(part);
|
|
493
|
+
const innerBlocks = current.get("innerBlocks");
|
|
494
|
+
if (innerBlocks) {
|
|
495
|
+
currentArray = innerBlocks;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return current;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// src/wordpress/api-client.ts
|
|
505
|
+
var WordPressApiClient, WordPressApiError;
|
|
506
|
+
var init_api_client = __esm({
|
|
507
|
+
"src/wordpress/api-client.ts"() {
|
|
508
|
+
"use strict";
|
|
509
|
+
WordPressApiClient = class {
|
|
510
|
+
baseUrl;
|
|
511
|
+
authHeader;
|
|
512
|
+
constructor(config) {
|
|
513
|
+
const siteUrl = config.siteUrl.replace(/\/+$/, "");
|
|
514
|
+
this.baseUrl = `${siteUrl}/wp-json`;
|
|
515
|
+
this.authHeader = `Basic ${btoa(config.username + ":" + config.appPassword)}`;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Validate the connection by fetching the current user.
|
|
519
|
+
* Tests both auth and API availability.
|
|
520
|
+
*/
|
|
521
|
+
async validateConnection() {
|
|
522
|
+
return this.getCurrentUser();
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Check that the sync endpoint exists.
|
|
526
|
+
* POSTs an empty rooms array to verify the endpoint responds.
|
|
527
|
+
*/
|
|
528
|
+
async validateSyncEndpoint() {
|
|
529
|
+
await this.sendSyncUpdate({ rooms: [] });
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get the current authenticated user.
|
|
533
|
+
* GET /wp/v2/users/me
|
|
534
|
+
*/
|
|
535
|
+
async getCurrentUser() {
|
|
536
|
+
return this.apiFetch("/wp/v2/users/me");
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* List posts with optional filters.
|
|
540
|
+
* GET /wp/v2/posts?status=...&search=...&per_page=...&context=edit
|
|
541
|
+
*/
|
|
542
|
+
async listPosts(options) {
|
|
543
|
+
const params = new URLSearchParams({ context: "edit" });
|
|
544
|
+
if (options?.status) {
|
|
545
|
+
params.set("status", options.status);
|
|
546
|
+
}
|
|
547
|
+
if (options?.search) {
|
|
548
|
+
params.set("search", options.search);
|
|
549
|
+
}
|
|
550
|
+
if (options?.perPage !== void 0) {
|
|
551
|
+
params.set("per_page", String(options.perPage));
|
|
552
|
+
}
|
|
553
|
+
return this.apiFetch(`/wp/v2/posts?${params.toString()}`);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Get a single post by ID.
|
|
557
|
+
* GET /wp/v2/posts/{id}?context=edit
|
|
558
|
+
*/
|
|
559
|
+
async getPost(id) {
|
|
560
|
+
return this.apiFetch(`/wp/v2/posts/${id}?context=edit`);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Create a new post.
|
|
564
|
+
* POST /wp/v2/posts
|
|
565
|
+
*/
|
|
566
|
+
async createPost(data) {
|
|
567
|
+
return this.apiFetch("/wp/v2/posts", {
|
|
568
|
+
method: "POST",
|
|
569
|
+
body: JSON.stringify(data)
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Send a sync payload and receive response.
|
|
574
|
+
* POST /wp-sync/v1/updates
|
|
575
|
+
*/
|
|
576
|
+
async sendSyncUpdate(payload) {
|
|
577
|
+
return this.apiFetch("/wp-sync/v1/updates", {
|
|
578
|
+
method: "POST",
|
|
579
|
+
body: JSON.stringify(payload)
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Produce a human-friendly error message for common failure modes.
|
|
584
|
+
*/
|
|
585
|
+
formatErrorMessage(path, status, body) {
|
|
586
|
+
if (status === 401 || status === 403) {
|
|
587
|
+
return `Authentication failed. Check your username and Application Password. (HTTP ${status})`;
|
|
588
|
+
}
|
|
589
|
+
if (status === 404 && path.startsWith("/wp-sync/")) {
|
|
590
|
+
return "Collaborative editing is not enabled. Enable it in Settings \u2192 Writing in your WordPress admin, then try again. (Requires WordPress 7.0 or later.)";
|
|
591
|
+
}
|
|
592
|
+
return `WordPress API error ${status}: ${body}`;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Internal fetch helper with auth and error handling.
|
|
596
|
+
*/
|
|
597
|
+
async apiFetch(path, options) {
|
|
598
|
+
const url = `${this.baseUrl}${path}`;
|
|
599
|
+
const headers = {
|
|
600
|
+
Authorization: this.authHeader,
|
|
601
|
+
Accept: "application/json"
|
|
602
|
+
};
|
|
603
|
+
if (options?.method === "POST" || options?.method === "PUT") {
|
|
604
|
+
headers["Content-Type"] = "application/json";
|
|
605
|
+
}
|
|
606
|
+
const response = await fetch(url, {
|
|
607
|
+
...options,
|
|
608
|
+
headers: {
|
|
609
|
+
...headers,
|
|
610
|
+
...options?.headers
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
if (!response.ok) {
|
|
614
|
+
let errorBody;
|
|
615
|
+
try {
|
|
616
|
+
errorBody = await response.text();
|
|
617
|
+
} catch {
|
|
618
|
+
errorBody = "(unable to read response body)";
|
|
619
|
+
}
|
|
620
|
+
const message = this.formatErrorMessage(path, response.status, errorBody);
|
|
621
|
+
throw new WordPressApiError(
|
|
622
|
+
message,
|
|
623
|
+
response.status,
|
|
624
|
+
errorBody
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
return await response.json();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
WordPressApiError = class extends Error {
|
|
631
|
+
constructor(message, status, body) {
|
|
632
|
+
super(message);
|
|
633
|
+
this.status = status;
|
|
634
|
+
this.body = body;
|
|
635
|
+
this.name = "WordPressApiError";
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// src/wordpress/sync-client.ts
|
|
642
|
+
var SyncClient;
|
|
643
|
+
var init_sync_client = __esm({
|
|
644
|
+
"src/wordpress/sync-client.ts"() {
|
|
645
|
+
"use strict";
|
|
646
|
+
SyncClient = class {
|
|
647
|
+
constructor(apiClient, config) {
|
|
648
|
+
this.apiClient = apiClient;
|
|
649
|
+
this.config = config;
|
|
650
|
+
this.currentBackoff = config.pollingInterval;
|
|
651
|
+
}
|
|
652
|
+
pollTimer = null;
|
|
653
|
+
endCursor = 0;
|
|
654
|
+
updateQueue = [];
|
|
655
|
+
queuePaused = true;
|
|
656
|
+
hasCollaborators = false;
|
|
657
|
+
currentBackoff;
|
|
658
|
+
isPolling = false;
|
|
659
|
+
pollInProgress = false;
|
|
660
|
+
flushRequested = false;
|
|
661
|
+
room = "";
|
|
662
|
+
clientId = 0;
|
|
663
|
+
callbacks = null;
|
|
664
|
+
firstPollResolve = null;
|
|
665
|
+
/**
|
|
666
|
+
* Returns a promise that resolves after the first poll cycle completes.
|
|
667
|
+
* Used to wait for initial sync state before loading content.
|
|
668
|
+
*/
|
|
669
|
+
waitForFirstPoll() {
|
|
670
|
+
return new Promise((resolve) => {
|
|
671
|
+
this.firstPollResolve = resolve;
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Start the polling loop for a room.
|
|
676
|
+
*
|
|
677
|
+
* @param room Room identifier, e.g. 'postType/post:123'
|
|
678
|
+
* @param clientId Y.Doc clientID
|
|
679
|
+
* @param initialUpdates Initial sync updates to send (sync_step1)
|
|
680
|
+
* @param callbacks Event callbacks
|
|
681
|
+
*/
|
|
682
|
+
start(room, clientId, initialUpdates, callbacks) {
|
|
683
|
+
this.room = room;
|
|
684
|
+
this.clientId = clientId;
|
|
685
|
+
this.callbacks = callbacks;
|
|
686
|
+
this.endCursor = 0;
|
|
687
|
+
this.queuePaused = true;
|
|
688
|
+
this.hasCollaborators = false;
|
|
689
|
+
this.currentBackoff = this.config.pollingInterval;
|
|
690
|
+
this.isPolling = true;
|
|
691
|
+
this.updateQueue = [...initialUpdates];
|
|
692
|
+
this.callbacks.onStatusChange("connecting");
|
|
693
|
+
this.pollTimer = setTimeout(() => this.poll(), 0);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Stop the polling loop.
|
|
697
|
+
*/
|
|
698
|
+
stop() {
|
|
699
|
+
this.isPolling = false;
|
|
700
|
+
if (this.pollTimer !== null) {
|
|
701
|
+
clearTimeout(this.pollTimer);
|
|
702
|
+
this.pollTimer = null;
|
|
703
|
+
}
|
|
704
|
+
this.callbacks?.onStatusChange("disconnected");
|
|
705
|
+
this.callbacks = null;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Flush the outgoing queue by triggering an immediate poll.
|
|
709
|
+
*
|
|
710
|
+
* If a poll is already in progress, sets a flag so that another poll
|
|
711
|
+
* is triggered immediately after the current one completes. This avoids
|
|
712
|
+
* concurrent poll execution while ensuring the flush is honoured.
|
|
713
|
+
*/
|
|
714
|
+
flushQueue() {
|
|
715
|
+
if (!this.isPolling) return;
|
|
716
|
+
if (this.pollInProgress) {
|
|
717
|
+
this.flushRequested = true;
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (this.pollTimer !== null) {
|
|
721
|
+
clearTimeout(this.pollTimer);
|
|
722
|
+
this.pollTimer = null;
|
|
723
|
+
}
|
|
724
|
+
this.pollTimer = setTimeout(() => this.poll(), 0);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Add an update to the outgoing queue.
|
|
728
|
+
*/
|
|
729
|
+
queueUpdate(update) {
|
|
730
|
+
this.updateQueue.push(update);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Get sync status info.
|
|
734
|
+
*/
|
|
735
|
+
getStatus() {
|
|
736
|
+
return {
|
|
737
|
+
isPolling: this.isPolling,
|
|
738
|
+
hasCollaborators: this.hasCollaborators,
|
|
739
|
+
queuePaused: this.queuePaused,
|
|
740
|
+
endCursor: this.endCursor,
|
|
741
|
+
queueSize: this.updateQueue.length
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Execute one poll cycle.
|
|
746
|
+
*/
|
|
747
|
+
async poll() {
|
|
748
|
+
if (!this.isPolling || !this.callbacks) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (this.pollInProgress) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
this.pollInProgress = true;
|
|
755
|
+
this.flushRequested = false;
|
|
756
|
+
const updates = this.updateQueue.splice(0);
|
|
757
|
+
const awareness = this.callbacks.getAwarenessState();
|
|
758
|
+
try {
|
|
759
|
+
const payload = {
|
|
760
|
+
rooms: [
|
|
761
|
+
{
|
|
762
|
+
room: this.room,
|
|
763
|
+
client_id: this.clientId,
|
|
764
|
+
after: this.endCursor,
|
|
765
|
+
awareness,
|
|
766
|
+
updates
|
|
767
|
+
}
|
|
768
|
+
]
|
|
769
|
+
};
|
|
770
|
+
const response = await this.apiClient.sendSyncUpdate(payload);
|
|
771
|
+
this.currentBackoff = this.hasCollaborators ? this.config.pollingIntervalWithCollaborators : this.config.pollingInterval;
|
|
772
|
+
this.callbacks.onStatusChange("connected");
|
|
773
|
+
this.processResponse(response);
|
|
774
|
+
if (this.firstPollResolve) {
|
|
775
|
+
this.firstPollResolve();
|
|
776
|
+
this.firstPollResolve = null;
|
|
777
|
+
}
|
|
778
|
+
} catch (error) {
|
|
779
|
+
const restorable = updates.filter(
|
|
780
|
+
(u) => u.type !== "compaction"
|
|
781
|
+
);
|
|
782
|
+
this.updateQueue.unshift(...restorable);
|
|
783
|
+
this.callbacks.onStatusChange("error");
|
|
784
|
+
this.currentBackoff = Math.min(
|
|
785
|
+
this.currentBackoff * 2,
|
|
786
|
+
this.config.maxErrorBackoff
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
this.pollInProgress = false;
|
|
790
|
+
if (this.flushRequested) {
|
|
791
|
+
this.flushRequested = false;
|
|
792
|
+
if (this.pollTimer !== null) {
|
|
793
|
+
clearTimeout(this.pollTimer);
|
|
794
|
+
this.pollTimer = null;
|
|
795
|
+
}
|
|
796
|
+
this.pollTimer = setTimeout(() => this.poll(), 0);
|
|
797
|
+
} else {
|
|
798
|
+
this.scheduleNextPoll();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Schedule the next poll using the current interval / backoff.
|
|
803
|
+
*/
|
|
804
|
+
scheduleNextPoll() {
|
|
805
|
+
if (!this.isPolling) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
this.pollTimer = setTimeout(() => this.poll(), this.currentBackoff);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Process a sync response from the server.
|
|
812
|
+
*/
|
|
813
|
+
processResponse(response) {
|
|
814
|
+
if (!this.callbacks) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const roomData = response.rooms.find((r) => r.room === this.room);
|
|
818
|
+
if (!roomData) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
this.endCursor = roomData.end_cursor;
|
|
822
|
+
this.callbacks.onAwareness(roomData.awareness);
|
|
823
|
+
const otherClients = Object.keys(roomData.awareness).filter(
|
|
824
|
+
(id) => Number(id) !== this.clientId && roomData.awareness[id] !== null
|
|
825
|
+
);
|
|
826
|
+
const hadCollaborators = this.hasCollaborators;
|
|
827
|
+
this.hasCollaborators = otherClients.length > 0;
|
|
828
|
+
if (this.hasCollaborators && !hadCollaborators) {
|
|
829
|
+
this.queuePaused = false;
|
|
830
|
+
}
|
|
831
|
+
this.currentBackoff = this.hasCollaborators ? this.config.pollingIntervalWithCollaborators : this.config.pollingInterval;
|
|
832
|
+
for (const update of roomData.updates) {
|
|
833
|
+
const reply = this.callbacks.onUpdate(update);
|
|
834
|
+
if (reply) {
|
|
835
|
+
this.updateQueue.push(reply);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (roomData.should_compact) {
|
|
839
|
+
const compactionUpdate = this.callbacks.onCompactionRequested();
|
|
840
|
+
this.updateQueue.push(compactionUpdate);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// src/wordpress/types.ts
|
|
848
|
+
var DEFAULT_SYNC_CONFIG;
|
|
849
|
+
var init_types2 = __esm({
|
|
850
|
+
"src/wordpress/types.ts"() {
|
|
851
|
+
"use strict";
|
|
852
|
+
DEFAULT_SYNC_CONFIG = {
|
|
853
|
+
pollingInterval: 1e3,
|
|
854
|
+
pollingIntervalWithCollaborators: 250,
|
|
855
|
+
maxErrorBackoff: 3e4
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// src/yjs/sync-protocol.ts
|
|
861
|
+
import * as Y3 from "yjs";
|
|
862
|
+
import * as syncProtocol from "y-protocols/sync";
|
|
863
|
+
import * as encoding from "lib0/encoding";
|
|
864
|
+
import * as decoding from "lib0/decoding";
|
|
865
|
+
function createSyncStep1(doc) {
|
|
866
|
+
const encoder = encoding.createEncoder();
|
|
867
|
+
syncProtocol.writeSyncStep1(encoder, doc);
|
|
868
|
+
const data = encoding.toUint8Array(encoder);
|
|
869
|
+
return {
|
|
870
|
+
type: "sync_step1" /* SYNC_STEP_1 */,
|
|
871
|
+
data: uint8ArrayToBase64(data)
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function createSyncStep2(doc, step1Data) {
|
|
875
|
+
const decoder = decoding.createDecoder(step1Data);
|
|
876
|
+
const encoder = encoding.createEncoder();
|
|
877
|
+
syncProtocol.readSyncMessage(decoder, encoder, doc, "sync");
|
|
878
|
+
const data = encoding.toUint8Array(encoder);
|
|
879
|
+
return {
|
|
880
|
+
type: "sync_step2" /* SYNC_STEP_2 */,
|
|
881
|
+
data: uint8ArrayToBase64(data)
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function processIncomingUpdate(doc, update) {
|
|
885
|
+
const rawData = base64ToUint8Array(update.data);
|
|
886
|
+
switch (update.type) {
|
|
887
|
+
case "sync_step1" /* SYNC_STEP_1 */: {
|
|
888
|
+
return createSyncStep2(doc, rawData);
|
|
889
|
+
}
|
|
890
|
+
case "sync_step2" /* SYNC_STEP_2 */: {
|
|
891
|
+
const decoder = decoding.createDecoder(rawData);
|
|
892
|
+
const encoder = encoding.createEncoder();
|
|
893
|
+
syncProtocol.readSyncMessage(decoder, encoder, doc, "sync");
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
case "update" /* UPDATE */:
|
|
897
|
+
case "compaction" /* COMPACTION */: {
|
|
898
|
+
Y3.applyUpdate(doc, rawData, "remote");
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
default:
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function createUpdateFromChange(update) {
|
|
906
|
+
return {
|
|
907
|
+
type: "update" /* UPDATE */,
|
|
908
|
+
data: uint8ArrayToBase64(update)
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function createCompactionUpdate(doc) {
|
|
912
|
+
const data = Y3.encodeStateAsUpdate(doc);
|
|
913
|
+
return {
|
|
914
|
+
type: "compaction" /* COMPACTION */,
|
|
915
|
+
data: uint8ArrayToBase64(data)
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function uint8ArrayToBase64(data) {
|
|
919
|
+
return Buffer.from(data).toString("base64");
|
|
920
|
+
}
|
|
921
|
+
function base64ToUint8Array(base64) {
|
|
922
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
923
|
+
}
|
|
924
|
+
var init_sync_protocol = __esm({
|
|
925
|
+
"src/yjs/sync-protocol.ts"() {
|
|
926
|
+
"use strict";
|
|
927
|
+
init_types2();
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// src/blocks/parser.ts
|
|
932
|
+
import { parse as wpParse } from "@wordpress/block-serialization-default-parser";
|
|
933
|
+
function parseBlocks(html) {
|
|
934
|
+
const raw = wpParse(html);
|
|
935
|
+
return raw.filter((block) => block.blockName !== null || block.innerHTML.trim() !== "").map(normalizeParsedBlock);
|
|
936
|
+
}
|
|
937
|
+
function parsedBlockToBlock(parsed) {
|
|
938
|
+
return {
|
|
939
|
+
name: parsed.name,
|
|
940
|
+
clientId: crypto.randomUUID(),
|
|
941
|
+
attributes: { ...parsed.attributes },
|
|
942
|
+
innerBlocks: parsed.innerBlocks.map(parsedBlockToBlock),
|
|
943
|
+
originalContent: parsed.originalContent
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function normalizeParsedBlock(raw) {
|
|
947
|
+
const blockName = raw.blockName ?? "core/freeform";
|
|
948
|
+
const commentAttrs = raw.attrs ?? {};
|
|
949
|
+
const extractedAttrs = extractAttributesFromHTML(
|
|
950
|
+
blockName,
|
|
951
|
+
raw.innerHTML,
|
|
952
|
+
commentAttrs
|
|
953
|
+
);
|
|
954
|
+
return {
|
|
955
|
+
name: blockName,
|
|
956
|
+
attributes: { ...commentAttrs, ...extractedAttrs },
|
|
957
|
+
innerBlocks: raw.innerBlocks.filter(
|
|
958
|
+
(block) => block.blockName !== null || block.innerHTML.trim() !== ""
|
|
959
|
+
).map(normalizeParsedBlock),
|
|
960
|
+
originalContent: raw.innerHTML
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function extractAttributesFromHTML(blockName, innerHTML, attrs) {
|
|
964
|
+
const extracted = {};
|
|
965
|
+
switch (blockName) {
|
|
966
|
+
case "core/paragraph": {
|
|
967
|
+
const match = innerHTML.match(/<p[^>]*>([\s\S]*?)<\/p>/);
|
|
968
|
+
if (match) {
|
|
969
|
+
extracted.content = match[1].trim();
|
|
970
|
+
}
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
case "core/heading": {
|
|
974
|
+
const match = innerHTML.match(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/);
|
|
975
|
+
if (match) {
|
|
976
|
+
extracted.content = match[1].trim();
|
|
977
|
+
}
|
|
978
|
+
if (attrs.level !== void 0) {
|
|
979
|
+
extracted.level = attrs.level;
|
|
980
|
+
}
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
case "core/list-item": {
|
|
984
|
+
const match = innerHTML.match(/<li[^>]*>([\s\S]*?)<\/li>/);
|
|
985
|
+
if (match) {
|
|
986
|
+
extracted.content = match[1].trim();
|
|
987
|
+
}
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
case "core/image": {
|
|
991
|
+
const imgMatch = innerHTML.match(/<img[^>]*>/);
|
|
992
|
+
if (imgMatch) {
|
|
993
|
+
const srcMatch = imgMatch[0].match(/src="([^"]*)"/);
|
|
994
|
+
if (srcMatch) {
|
|
995
|
+
extracted.url = srcMatch[1];
|
|
996
|
+
}
|
|
997
|
+
const altMatch = imgMatch[0].match(/alt="([^"]*)"/);
|
|
998
|
+
if (altMatch) {
|
|
999
|
+
extracted.alt = altMatch[1];
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
case "core/button": {
|
|
1005
|
+
const match = innerHTML.match(/<a[^>]*>([\s\S]*?)<\/a>/);
|
|
1006
|
+
if (match) {
|
|
1007
|
+
extracted.text = match[1].trim();
|
|
1008
|
+
}
|
|
1009
|
+
const hrefMatch = innerHTML.match(/<a[^>]*href="([^"]*)"[^>]*>/);
|
|
1010
|
+
if (hrefMatch) {
|
|
1011
|
+
extracted.url = hrefMatch[1];
|
|
1012
|
+
}
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return extracted;
|
|
1017
|
+
}
|
|
1018
|
+
var init_parser = __esm({
|
|
1019
|
+
"src/blocks/parser.ts"() {
|
|
1020
|
+
"use strict";
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// src/blocks/renderer.ts
|
|
1025
|
+
function renderPost(title, blocks) {
|
|
1026
|
+
const parts = [];
|
|
1027
|
+
parts.push(`Title: "${title}"`);
|
|
1028
|
+
if (blocks.length > 0) {
|
|
1029
|
+
parts.push("");
|
|
1030
|
+
parts.push(renderBlockList(blocks));
|
|
1031
|
+
}
|
|
1032
|
+
return parts.join("\n");
|
|
1033
|
+
}
|
|
1034
|
+
function renderBlock(block, index) {
|
|
1035
|
+
const displayAttrs = getDisplayAttributes(block);
|
|
1036
|
+
const attrStr = Object.entries(displayAttrs).map(([k, v]) => `${k}=${typeof v === "string" ? `"${v}"` : v}`).join(", ");
|
|
1037
|
+
const header = attrStr ? `[${index}] ${block.name} (${attrStr})` : `[${index}] ${block.name}`;
|
|
1038
|
+
const lines = [header];
|
|
1039
|
+
const textContent = getBlockTextContent(block);
|
|
1040
|
+
if (textContent) {
|
|
1041
|
+
lines.push(` "${textContent}"`);
|
|
1042
|
+
}
|
|
1043
|
+
if (block.innerBlocks.length > 0) {
|
|
1044
|
+
const innerRendered = renderBlockList(block.innerBlocks, index, 1);
|
|
1045
|
+
lines.push(innerRendered);
|
|
1046
|
+
}
|
|
1047
|
+
return lines.join("\n");
|
|
1048
|
+
}
|
|
1049
|
+
function renderBlockList(blocks, parentIndex, indent = 0) {
|
|
1050
|
+
const indentStr = " ".repeat(indent);
|
|
1051
|
+
return blocks.map((block, i) => {
|
|
1052
|
+
const index = parentIndex !== void 0 ? `${parentIndex}.${i}` : String(i);
|
|
1053
|
+
const rendered = renderBlock(block, index);
|
|
1054
|
+
if (indent > 0) {
|
|
1055
|
+
return rendered.split("\n").map((line) => indentStr + line).join("\n");
|
|
1056
|
+
}
|
|
1057
|
+
return rendered;
|
|
1058
|
+
}).join("\n\n");
|
|
1059
|
+
}
|
|
1060
|
+
function getBlockTextContent(block) {
|
|
1061
|
+
for (const key of ["content", "text", "value", "citation"]) {
|
|
1062
|
+
const val = block.attributes[key];
|
|
1063
|
+
if (typeof val === "string" && val.length > 0) {
|
|
1064
|
+
return val;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return "";
|
|
1068
|
+
}
|
|
1069
|
+
function getDisplayAttributes(block) {
|
|
1070
|
+
const skipKeys = /* @__PURE__ */ new Set(["content", "text", "value", "citation"]);
|
|
1071
|
+
const result = {};
|
|
1072
|
+
for (const [key, val] of Object.entries(block.attributes)) {
|
|
1073
|
+
if (skipKeys.has(key)) continue;
|
|
1074
|
+
if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
|
|
1075
|
+
result[key] = val;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return result;
|
|
1079
|
+
}
|
|
1080
|
+
var init_renderer = __esm({
|
|
1081
|
+
"src/blocks/renderer.ts"() {
|
|
1082
|
+
"use strict";
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// src/session/awareness.ts
|
|
1087
|
+
function buildAwarenessState(user) {
|
|
1088
|
+
return {
|
|
1089
|
+
collaboratorInfo: {
|
|
1090
|
+
id: user.id,
|
|
1091
|
+
name: `${user.name} (Claude)`,
|
|
1092
|
+
slug: user.slug,
|
|
1093
|
+
avatar_urls: user.avatar_urls ?? {},
|
|
1094
|
+
browserType: "Claude Code MCP",
|
|
1095
|
+
enteredAt: Date.now()
|
|
1096
|
+
},
|
|
1097
|
+
// editorState with selection is required for Gutenberg to recognize us
|
|
1098
|
+
// as an active editor and process our CRDT updates in the live session.
|
|
1099
|
+
editorState: {
|
|
1100
|
+
selection: { type: "none" }
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
function parseCollaborators(awarenessState, ownClientId) {
|
|
1105
|
+
const collaborators = [];
|
|
1106
|
+
for (const [clientIdStr, state] of Object.entries(awarenessState)) {
|
|
1107
|
+
const clientId = Number(clientIdStr);
|
|
1108
|
+
if (clientId === ownClientId) {
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
if (state === null || state === void 0) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const info = state.collaboratorInfo;
|
|
1115
|
+
if (info) {
|
|
1116
|
+
collaborators.push(info);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return collaborators;
|
|
1120
|
+
}
|
|
1121
|
+
var init_awareness = __esm({
|
|
1122
|
+
"src/session/awareness.ts"() {
|
|
1123
|
+
"use strict";
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// src/session/session-manager.ts
|
|
1128
|
+
import * as Y4 from "yjs";
|
|
1129
|
+
function prepareBlockTree(input, indexPrefix) {
|
|
1130
|
+
const defaults = getDefaultAttributes(input.name);
|
|
1131
|
+
const attrs = { ...defaults, ...input.attributes };
|
|
1132
|
+
const streamTargets = [];
|
|
1133
|
+
if (input.content !== void 0) {
|
|
1134
|
+
if (isRichTextAttribute(input.name, "content") && input.content.length >= STREAM_THRESHOLD) {
|
|
1135
|
+
streamTargets.push({ blockIndex: indexPrefix, attrName: "content", value: input.content });
|
|
1136
|
+
attrs.content = "";
|
|
1137
|
+
} else {
|
|
1138
|
+
attrs.content = input.content;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
1142
|
+
if (key !== "content" && isRichTextAttribute(input.name, key) && typeof value === "string" && value.length >= STREAM_THRESHOLD) {
|
|
1143
|
+
streamTargets.push({ blockIndex: indexPrefix, attrName: key, value });
|
|
1144
|
+
attrs[key] = "";
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const innerBlocks = [];
|
|
1148
|
+
if (input.innerBlocks) {
|
|
1149
|
+
for (let i = 0; i < input.innerBlocks.length; i++) {
|
|
1150
|
+
const childIndex = `${indexPrefix}.${i}`;
|
|
1151
|
+
const prepared = prepareBlockTree(input.innerBlocks[i], childIndex);
|
|
1152
|
+
innerBlocks.push(prepared.block);
|
|
1153
|
+
streamTargets.push(...prepared.streamTargets);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const block = {
|
|
1157
|
+
name: input.name,
|
|
1158
|
+
clientId: crypto.randomUUID(),
|
|
1159
|
+
attributes: attrs,
|
|
1160
|
+
innerBlocks,
|
|
1161
|
+
isValid: true
|
|
1162
|
+
};
|
|
1163
|
+
return { block, streamTargets };
|
|
1164
|
+
}
|
|
1165
|
+
var LOCAL_ORIGIN, STREAM_CHUNK_SIZE_MIN, STREAM_CHUNK_SIZE_MAX, STREAM_CHUNK_DELAY_MS, STREAM_THRESHOLD, SessionManager;
|
|
1166
|
+
var init_session_manager = __esm({
|
|
1167
|
+
"src/session/session-manager.ts"() {
|
|
1168
|
+
"use strict";
|
|
1169
|
+
init_document_manager();
|
|
1170
|
+
init_api_client();
|
|
1171
|
+
init_sync_client();
|
|
1172
|
+
init_sync_protocol();
|
|
1173
|
+
init_block_converter();
|
|
1174
|
+
init_parser();
|
|
1175
|
+
init_renderer();
|
|
1176
|
+
init_awareness();
|
|
1177
|
+
init_types2();
|
|
1178
|
+
init_types();
|
|
1179
|
+
LOCAL_ORIGIN = "local";
|
|
1180
|
+
STREAM_CHUNK_SIZE_MIN = 2;
|
|
1181
|
+
STREAM_CHUNK_SIZE_MAX = 6;
|
|
1182
|
+
STREAM_CHUNK_DELAY_MS = 200;
|
|
1183
|
+
STREAM_THRESHOLD = 20;
|
|
1184
|
+
SessionManager = class {
|
|
1185
|
+
apiClient = null;
|
|
1186
|
+
syncClient = null;
|
|
1187
|
+
documentManager;
|
|
1188
|
+
doc = null;
|
|
1189
|
+
user = null;
|
|
1190
|
+
currentPost = null;
|
|
1191
|
+
state = "disconnected";
|
|
1192
|
+
awarenessState = null;
|
|
1193
|
+
collaborators = [];
|
|
1194
|
+
updateHandler = null;
|
|
1195
|
+
/** Max time (ms) to wait for sync to populate the doc before loading from REST API. Set to 0 in tests. */
|
|
1196
|
+
syncWaitTimeout = 5e3;
|
|
1197
|
+
constructor() {
|
|
1198
|
+
this.documentManager = new DocumentManager();
|
|
1199
|
+
}
|
|
1200
|
+
// --- Connection ---
|
|
1201
|
+
/**
|
|
1202
|
+
* Connect to a WordPress site.
|
|
1203
|
+
* Validates credentials and sync endpoint availability.
|
|
1204
|
+
*/
|
|
1205
|
+
async connect(config) {
|
|
1206
|
+
this.apiClient = new WordPressApiClient(config);
|
|
1207
|
+
const user = await this.apiClient.validateConnection();
|
|
1208
|
+
this.user = user;
|
|
1209
|
+
await this.apiClient.validateSyncEndpoint();
|
|
1210
|
+
this.awarenessState = buildAwarenessState(user);
|
|
1211
|
+
this.state = "connected";
|
|
1212
|
+
return user;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Disconnect from the WordPress site.
|
|
1216
|
+
*/
|
|
1217
|
+
disconnect() {
|
|
1218
|
+
if (this.state === "editing") {
|
|
1219
|
+
this.closePost();
|
|
1220
|
+
}
|
|
1221
|
+
this.apiClient = null;
|
|
1222
|
+
this.user = null;
|
|
1223
|
+
this.awarenessState = null;
|
|
1224
|
+
this.collaborators = [];
|
|
1225
|
+
this.state = "disconnected";
|
|
1226
|
+
}
|
|
1227
|
+
// --- Posts ---
|
|
1228
|
+
/**
|
|
1229
|
+
* List posts (delegates to API client).
|
|
1230
|
+
*/
|
|
1231
|
+
async listPosts(options) {
|
|
1232
|
+
this.requireState("connected", "editing");
|
|
1233
|
+
return this.apiClient.listPosts(options);
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Open a post for collaborative editing.
|
|
1237
|
+
* Creates Y.Doc, loads initial content, starts sync.
|
|
1238
|
+
*/
|
|
1239
|
+
async openPost(postId) {
|
|
1240
|
+
this.requireState("connected");
|
|
1241
|
+
const post = await this.apiClient.getPost(postId);
|
|
1242
|
+
this.currentPost = post;
|
|
1243
|
+
const doc = this.documentManager.createDoc();
|
|
1244
|
+
this.doc = doc;
|
|
1245
|
+
const syncClient = new SyncClient(this.apiClient, { ...DEFAULT_SYNC_CONFIG });
|
|
1246
|
+
this.syncClient = syncClient;
|
|
1247
|
+
const room = `postType/${post.type}:${postId}`;
|
|
1248
|
+
const initialUpdates = [createSyncStep1(doc)];
|
|
1249
|
+
syncClient.start(room, doc.clientID, initialUpdates, {
|
|
1250
|
+
onUpdate: (update) => {
|
|
1251
|
+
try {
|
|
1252
|
+
return processIncomingUpdate(doc, update);
|
|
1253
|
+
} catch {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
onAwareness: (awarenessState) => {
|
|
1258
|
+
this.collaborators = parseCollaborators(awarenessState, doc.clientID);
|
|
1259
|
+
},
|
|
1260
|
+
onStatusChange: () => {
|
|
1261
|
+
},
|
|
1262
|
+
onCompactionRequested: () => {
|
|
1263
|
+
return createCompactionUpdate(doc);
|
|
1264
|
+
},
|
|
1265
|
+
getAwarenessState: () => {
|
|
1266
|
+
return this.awarenessState;
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
if (this.syncWaitTimeout > 0) {
|
|
1270
|
+
await new Promise((resolve) => {
|
|
1271
|
+
let resolved = false;
|
|
1272
|
+
const done = () => {
|
|
1273
|
+
if (!resolved) {
|
|
1274
|
+
resolved = true;
|
|
1275
|
+
doc.off("update", onDocUpdate);
|
|
1276
|
+
resolve();
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
const timeout = setTimeout(done, this.syncWaitTimeout);
|
|
1280
|
+
const onDocUpdate = () => {
|
|
1281
|
+
const blocks = this.documentManager.getBlocks(doc);
|
|
1282
|
+
const title = this.documentManager.getTitle(doc);
|
|
1283
|
+
if (blocks.length > 0 || title.length > 0) {
|
|
1284
|
+
clearTimeout(timeout);
|
|
1285
|
+
done();
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
doc.on("update", onDocUpdate);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
const existingBlocks = this.documentManager.getBlocks(doc);
|
|
1292
|
+
if (existingBlocks.length === 0) {
|
|
1293
|
+
doc.transact(() => {
|
|
1294
|
+
if (post.title.raw) {
|
|
1295
|
+
this.documentManager.setTitle(doc, post.title.raw);
|
|
1296
|
+
}
|
|
1297
|
+
if (post.content.raw) {
|
|
1298
|
+
const parsedBlocks = parseBlocks(post.content.raw);
|
|
1299
|
+
const blocks = parsedBlocks.map(parsedBlockToBlock);
|
|
1300
|
+
this.documentManager.setBlocks(doc, blocks);
|
|
1301
|
+
this.documentManager.setContent(doc, post.content.raw);
|
|
1302
|
+
}
|
|
1303
|
+
if (post.excerpt.raw) {
|
|
1304
|
+
this.documentManager.setProperty(doc, "excerpt", post.excerpt.raw);
|
|
1305
|
+
}
|
|
1306
|
+
this.documentManager.setProperty(doc, "status", post.status);
|
|
1307
|
+
this.documentManager.setProperty(doc, "slug", post.slug);
|
|
1308
|
+
this.documentManager.setProperty(doc, "author", post.author);
|
|
1309
|
+
}, LOCAL_ORIGIN);
|
|
1310
|
+
}
|
|
1311
|
+
this.updateHandler = (update, origin) => {
|
|
1312
|
+
if (origin === LOCAL_ORIGIN) {
|
|
1313
|
+
const syncUpdate = createUpdateFromChange(update);
|
|
1314
|
+
syncClient.queueUpdate(syncUpdate);
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
doc.on("update", this.updateHandler);
|
|
1318
|
+
this.state = "editing";
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Create a new post and open it for editing.
|
|
1322
|
+
*/
|
|
1323
|
+
async createPost(data) {
|
|
1324
|
+
this.requireState("connected");
|
|
1325
|
+
const post = await this.apiClient.createPost({
|
|
1326
|
+
title: data.title,
|
|
1327
|
+
content: data.content,
|
|
1328
|
+
status: "draft"
|
|
1329
|
+
});
|
|
1330
|
+
await this.openPost(post.id);
|
|
1331
|
+
return post;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Close the currently open post (stop sync).
|
|
1335
|
+
*/
|
|
1336
|
+
closePost() {
|
|
1337
|
+
if (this.syncClient) {
|
|
1338
|
+
this.syncClient.stop();
|
|
1339
|
+
this.syncClient = null;
|
|
1340
|
+
}
|
|
1341
|
+
if (this.doc && this.updateHandler) {
|
|
1342
|
+
this.doc.off("update", this.updateHandler);
|
|
1343
|
+
this.updateHandler = null;
|
|
1344
|
+
}
|
|
1345
|
+
this.doc = null;
|
|
1346
|
+
this.currentPost = null;
|
|
1347
|
+
this.collaborators = [];
|
|
1348
|
+
this.state = "connected";
|
|
1349
|
+
}
|
|
1350
|
+
// --- Reading ---
|
|
1351
|
+
/**
|
|
1352
|
+
* Render the current post as Claude-friendly text.
|
|
1353
|
+
*/
|
|
1354
|
+
readPost() {
|
|
1355
|
+
this.requireState("editing");
|
|
1356
|
+
const title = this.documentManager.getTitle(this.doc);
|
|
1357
|
+
const blocks = this.documentManager.getBlocks(this.doc);
|
|
1358
|
+
return renderPost(title, blocks);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Read a specific block by index (dot notation).
|
|
1362
|
+
*/
|
|
1363
|
+
readBlock(index) {
|
|
1364
|
+
this.requireState("editing");
|
|
1365
|
+
const block = this.documentManager.getBlockByIndex(this.doc, index);
|
|
1366
|
+
if (!block) {
|
|
1367
|
+
throw new Error(`Block not found at index ${index}`);
|
|
1368
|
+
}
|
|
1369
|
+
return renderBlock(block, index);
|
|
1370
|
+
}
|
|
1371
|
+
// --- Editing ---
|
|
1372
|
+
/**
|
|
1373
|
+
* Update a block's content and/or attributes.
|
|
1374
|
+
*
|
|
1375
|
+
* Rich-text attributes that exceed the streaming threshold are streamed
|
|
1376
|
+
* in chunks so the browser sees progressive updates (like fast typing).
|
|
1377
|
+
* Non-rich-text and short changes are applied atomically.
|
|
1378
|
+
*/
|
|
1379
|
+
async updateBlock(index, changes) {
|
|
1380
|
+
this.requireState("editing");
|
|
1381
|
+
this.updateCursorPosition(index);
|
|
1382
|
+
const block = this.documentManager.getBlockByIndex(this.doc, index);
|
|
1383
|
+
if (!block) return;
|
|
1384
|
+
const streamTargets = [];
|
|
1385
|
+
const atomicChanges = {};
|
|
1386
|
+
if (changes.content !== void 0) {
|
|
1387
|
+
if (isRichTextAttribute(block.name, "content") && changes.content.length >= STREAM_THRESHOLD) {
|
|
1388
|
+
streamTargets.push({ attrName: "content", newValue: changes.content });
|
|
1389
|
+
} else {
|
|
1390
|
+
atomicChanges.content = changes.content;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (changes.attributes) {
|
|
1394
|
+
const atomicAttrs = {};
|
|
1395
|
+
for (const [key, value] of Object.entries(changes.attributes)) {
|
|
1396
|
+
if (isRichTextAttribute(block.name, key) && typeof value === "string" && value.length >= STREAM_THRESHOLD) {
|
|
1397
|
+
streamTargets.push({ attrName: key, newValue: value });
|
|
1398
|
+
} else {
|
|
1399
|
+
atomicAttrs[key] = value;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (Object.keys(atomicAttrs).length > 0) {
|
|
1403
|
+
atomicChanges.attributes = atomicAttrs;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
if (atomicChanges.content !== void 0 || atomicChanges.attributes) {
|
|
1407
|
+
this.doc.transact(() => {
|
|
1408
|
+
this.documentManager.updateBlock(this.doc, index, atomicChanges);
|
|
1409
|
+
}, LOCAL_ORIGIN);
|
|
1410
|
+
}
|
|
1411
|
+
for (const target of streamTargets) {
|
|
1412
|
+
let ytext = this.documentManager.getBlockAttributeYText(this.doc, index, target.attrName);
|
|
1413
|
+
if (!ytext) {
|
|
1414
|
+
this.doc.transact(() => {
|
|
1415
|
+
this.documentManager.updateBlock(this.doc, index, {
|
|
1416
|
+
...target.attrName === "content" ? { content: "" } : { attributes: { [target.attrName]: "" } }
|
|
1417
|
+
});
|
|
1418
|
+
}, LOCAL_ORIGIN);
|
|
1419
|
+
ytext = this.documentManager.getBlockAttributeYText(this.doc, index, target.attrName);
|
|
1420
|
+
}
|
|
1421
|
+
if (ytext) {
|
|
1422
|
+
await this.streamTextToYText(ytext, target.newValue, index);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Insert a new block at position.
|
|
1428
|
+
*
|
|
1429
|
+
* The block structure (with empty content) is inserted atomically,
|
|
1430
|
+
* then rich-text content is streamed in progressively.
|
|
1431
|
+
* Supports recursive inner blocks.
|
|
1432
|
+
*/
|
|
1433
|
+
async insertBlock(position, block) {
|
|
1434
|
+
this.requireState("editing");
|
|
1435
|
+
const blockIndex = String(position);
|
|
1436
|
+
const { block: fullBlock, streamTargets } = prepareBlockTree(block, blockIndex);
|
|
1437
|
+
this.doc.transact(() => {
|
|
1438
|
+
this.documentManager.insertBlock(this.doc, position, fullBlock);
|
|
1439
|
+
}, LOCAL_ORIGIN);
|
|
1440
|
+
await this.streamTargets(streamTargets);
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Remove blocks starting at index.
|
|
1444
|
+
*/
|
|
1445
|
+
removeBlocks(startIndex, count) {
|
|
1446
|
+
this.requireState("editing");
|
|
1447
|
+
this.doc.transact(() => {
|
|
1448
|
+
this.documentManager.removeBlocks(this.doc, startIndex, count);
|
|
1449
|
+
}, LOCAL_ORIGIN);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Move a block from one position to another.
|
|
1453
|
+
*/
|
|
1454
|
+
moveBlock(fromIndex, toIndex) {
|
|
1455
|
+
this.requireState("editing");
|
|
1456
|
+
this.doc.transact(() => {
|
|
1457
|
+
this.documentManager.moveBlock(this.doc, fromIndex, toIndex);
|
|
1458
|
+
}, LOCAL_ORIGIN);
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Replace a range of blocks with new ones.
|
|
1462
|
+
*
|
|
1463
|
+
* Old blocks are removed and new block structures (with empty content)
|
|
1464
|
+
* are inserted atomically. Rich-text content is then streamed progressively.
|
|
1465
|
+
*/
|
|
1466
|
+
async replaceBlocks(startIndex, count, newBlocks) {
|
|
1467
|
+
this.requireState("editing");
|
|
1468
|
+
const allStreamTargets = [];
|
|
1469
|
+
const fullBlocks = newBlocks.map((b, i) => {
|
|
1470
|
+
const blockIndex = String(startIndex + i);
|
|
1471
|
+
const { block, streamTargets } = prepareBlockTree(b, blockIndex);
|
|
1472
|
+
allStreamTargets.push(...streamTargets);
|
|
1473
|
+
return block;
|
|
1474
|
+
});
|
|
1475
|
+
this.doc.transact(() => {
|
|
1476
|
+
this.documentManager.removeBlocks(this.doc, startIndex, count);
|
|
1477
|
+
for (let i = 0; i < fullBlocks.length; i++) {
|
|
1478
|
+
this.documentManager.insertBlock(
|
|
1479
|
+
this.doc,
|
|
1480
|
+
startIndex + i,
|
|
1481
|
+
fullBlocks[i]
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
}, LOCAL_ORIGIN);
|
|
1485
|
+
await this.streamTargets(allStreamTargets);
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Insert a block as an inner block of an existing block.
|
|
1489
|
+
*/
|
|
1490
|
+
async insertInnerBlock(parentIndex, position, block) {
|
|
1491
|
+
this.requireState("editing");
|
|
1492
|
+
const blockIndex = `${parentIndex}.${position}`;
|
|
1493
|
+
const { block: fullBlock, streamTargets } = prepareBlockTree(block, blockIndex);
|
|
1494
|
+
this.doc.transact(() => {
|
|
1495
|
+
this.documentManager.insertInnerBlock(this.doc, parentIndex, position, fullBlock);
|
|
1496
|
+
}, LOCAL_ORIGIN);
|
|
1497
|
+
await this.streamTargets(streamTargets);
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Remove inner blocks from an existing block.
|
|
1501
|
+
*/
|
|
1502
|
+
removeInnerBlocks(parentIndex, startIndex, count) {
|
|
1503
|
+
this.requireState("editing");
|
|
1504
|
+
this.doc.transact(() => {
|
|
1505
|
+
this.documentManager.removeInnerBlocks(this.doc, parentIndex, startIndex, count);
|
|
1506
|
+
}, LOCAL_ORIGIN);
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Set the post title.
|
|
1510
|
+
*
|
|
1511
|
+
* Long titles are streamed progressively; short titles are applied atomically.
|
|
1512
|
+
*/
|
|
1513
|
+
async setTitle(title) {
|
|
1514
|
+
this.requireState("editing");
|
|
1515
|
+
if (title.length < STREAM_THRESHOLD) {
|
|
1516
|
+
this.doc.transact(() => {
|
|
1517
|
+
this.documentManager.setTitle(this.doc, title);
|
|
1518
|
+
}, LOCAL_ORIGIN);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const documentMap = this.documentManager.getDocumentMap(this.doc);
|
|
1522
|
+
let ytext = documentMap.get("title");
|
|
1523
|
+
if (!(ytext instanceof Y4.Text)) {
|
|
1524
|
+
this.doc.transact(() => {
|
|
1525
|
+
const newYText = new Y4.Text();
|
|
1526
|
+
documentMap.set("title", newYText);
|
|
1527
|
+
}, LOCAL_ORIGIN);
|
|
1528
|
+
ytext = documentMap.get("title");
|
|
1529
|
+
}
|
|
1530
|
+
if (ytext instanceof Y4.Text) {
|
|
1531
|
+
await this.streamTextToYText(ytext, title);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Trigger a save.
|
|
1536
|
+
*/
|
|
1537
|
+
save() {
|
|
1538
|
+
this.requireState("editing");
|
|
1539
|
+
this.doc.transact(() => {
|
|
1540
|
+
this.documentManager.markSaved(this.doc);
|
|
1541
|
+
}, LOCAL_ORIGIN);
|
|
1542
|
+
}
|
|
1543
|
+
// --- Status ---
|
|
1544
|
+
getState() {
|
|
1545
|
+
return this.state;
|
|
1546
|
+
}
|
|
1547
|
+
getSyncStatus() {
|
|
1548
|
+
if (!this.syncClient) {
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
const status = this.syncClient.getStatus();
|
|
1552
|
+
return {
|
|
1553
|
+
isPolling: status.isPolling,
|
|
1554
|
+
hasCollaborators: status.hasCollaborators,
|
|
1555
|
+
queueSize: status.queueSize
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
getCollaborators() {
|
|
1559
|
+
return this.collaborators;
|
|
1560
|
+
}
|
|
1561
|
+
getCurrentPost() {
|
|
1562
|
+
return this.currentPost;
|
|
1563
|
+
}
|
|
1564
|
+
getUser() {
|
|
1565
|
+
return this.user;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Stream text into a Y.Text in chunks, flushing the sync client between
|
|
1569
|
+
* each chunk so the browser sees progressive updates (like fast typing).
|
|
1570
|
+
*
|
|
1571
|
+
* 1. Compute the delta between the current and target text.
|
|
1572
|
+
* 2. Apply retain + delete atomically (old text removed immediately).
|
|
1573
|
+
* 3. Split the insert text into HTML-safe chunks (~20 chars each).
|
|
1574
|
+
* 4. For each chunk: apply in its own transaction, flush, and delay.
|
|
1575
|
+
*/
|
|
1576
|
+
/**
|
|
1577
|
+
* Stream a list of targets (from prepareBlockTree) into their Y.Text instances.
|
|
1578
|
+
*/
|
|
1579
|
+
async streamTargets(targets) {
|
|
1580
|
+
for (const target of targets) {
|
|
1581
|
+
let ytext = this.documentManager.getBlockAttributeYText(this.doc, target.blockIndex, target.attrName);
|
|
1582
|
+
if (!ytext) {
|
|
1583
|
+
this.doc.transact(() => {
|
|
1584
|
+
this.documentManager.updateBlock(this.doc, target.blockIndex, {
|
|
1585
|
+
...target.attrName === "content" ? { content: "" } : { attributes: { [target.attrName]: "" } }
|
|
1586
|
+
});
|
|
1587
|
+
}, LOCAL_ORIGIN);
|
|
1588
|
+
ytext = this.documentManager.getBlockAttributeYText(this.doc, target.blockIndex, target.attrName);
|
|
1589
|
+
}
|
|
1590
|
+
if (ytext) {
|
|
1591
|
+
await this.streamTextToYText(ytext, target.value, target.blockIndex);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
async streamTextToYText(ytext, newValue, blockIndex) {
|
|
1596
|
+
const oldValue = ytext.toString();
|
|
1597
|
+
const delta = computeTextDelta(oldValue, newValue);
|
|
1598
|
+
if (!delta) return;
|
|
1599
|
+
if (blockIndex !== void 0) {
|
|
1600
|
+
this.updateCursorPosition(blockIndex);
|
|
1601
|
+
}
|
|
1602
|
+
if (delta.deleteCount > 0) {
|
|
1603
|
+
this.doc.transact(() => {
|
|
1604
|
+
const ops = [];
|
|
1605
|
+
if (delta.prefixLen > 0) ops.push({ retain: delta.prefixLen });
|
|
1606
|
+
ops.push({ delete: delta.deleteCount });
|
|
1607
|
+
ytext.applyDelta(ops);
|
|
1608
|
+
}, LOCAL_ORIGIN);
|
|
1609
|
+
if (this.syncClient) {
|
|
1610
|
+
this.syncClient.flushQueue();
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (delta.insertText.length === 0) return;
|
|
1614
|
+
if (delta.insertText.length < STREAM_THRESHOLD) {
|
|
1615
|
+
this.doc.transact(() => {
|
|
1616
|
+
const ops = [];
|
|
1617
|
+
if (delta.prefixLen > 0) ops.push({ retain: delta.prefixLen });
|
|
1618
|
+
ops.push({ insert: delta.insertText });
|
|
1619
|
+
ytext.applyDelta(ops);
|
|
1620
|
+
}, LOCAL_ORIGIN);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
let offset = 0;
|
|
1624
|
+
let insertPos = delta.prefixLen;
|
|
1625
|
+
let nextInsertRelPos = null;
|
|
1626
|
+
while (offset < delta.insertText.length) {
|
|
1627
|
+
if (!this.doc || !this.syncClient) return;
|
|
1628
|
+
if (nextInsertRelPos) {
|
|
1629
|
+
const absPos = Y4.createAbsolutePositionFromRelativePosition(nextInsertRelPos, this.doc);
|
|
1630
|
+
if (absPos) {
|
|
1631
|
+
insertPos = absPos.index;
|
|
1632
|
+
}
|
|
1633
|
+
nextInsertRelPos = null;
|
|
1634
|
+
}
|
|
1635
|
+
const chunkSize = STREAM_CHUNK_SIZE_MIN + Math.floor(Math.random() * (STREAM_CHUNK_SIZE_MAX - STREAM_CHUNK_SIZE_MIN + 1));
|
|
1636
|
+
const chunkEnd = findHtmlSafeChunkEnd(delta.insertText, offset, chunkSize);
|
|
1637
|
+
const chunk = delta.insertText.slice(offset, chunkEnd);
|
|
1638
|
+
this.doc.transact(() => {
|
|
1639
|
+
const ops = [];
|
|
1640
|
+
if (insertPos > 0) ops.push({ retain: insertPos });
|
|
1641
|
+
ops.push({ insert: chunk });
|
|
1642
|
+
ytext.applyDelta(ops);
|
|
1643
|
+
}, LOCAL_ORIGIN);
|
|
1644
|
+
insertPos += chunk.length;
|
|
1645
|
+
nextInsertRelPos = Y4.createRelativePositionFromTypeIndex(ytext, insertPos);
|
|
1646
|
+
offset = chunkEnd;
|
|
1647
|
+
this.updateCursorOffset(insertPos);
|
|
1648
|
+
if (offset < delta.insertText.length) {
|
|
1649
|
+
this.syncClient.flushQueue();
|
|
1650
|
+
await new Promise((resolve) => setTimeout(resolve, STREAM_CHUNK_DELAY_MS));
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Update the awareness cursor to point to a block's Y.Text type.
|
|
1656
|
+
* References the Y.Text type itself (not items within it) so the
|
|
1657
|
+
* cursor always resolves — even after updateYText deletes all items.
|
|
1658
|
+
*/
|
|
1659
|
+
updateCursorPosition(blockIndex) {
|
|
1660
|
+
if (!this.doc || !this.user) return;
|
|
1661
|
+
const ytext = this.documentManager.getBlockContentYText(this.doc, blockIndex);
|
|
1662
|
+
if (!ytext) return;
|
|
1663
|
+
const typeItem = ytext._item;
|
|
1664
|
+
if (!typeItem?.id) return;
|
|
1665
|
+
const relPosJSON = {
|
|
1666
|
+
type: { client: typeItem.id.client, clock: typeItem.id.clock },
|
|
1667
|
+
tname: null,
|
|
1668
|
+
item: null,
|
|
1669
|
+
assoc: 0
|
|
1670
|
+
};
|
|
1671
|
+
const enteredAt = this.awarenessState?.collaboratorInfo.enteredAt ?? Date.now();
|
|
1672
|
+
this.awarenessState = {
|
|
1673
|
+
collaboratorInfo: {
|
|
1674
|
+
id: this.user.id,
|
|
1675
|
+
name: `${this.user.name} (Claude)`,
|
|
1676
|
+
slug: this.user.slug,
|
|
1677
|
+
avatar_urls: this.user.avatar_urls ?? {},
|
|
1678
|
+
browserType: "Claude Code MCP",
|
|
1679
|
+
enteredAt
|
|
1680
|
+
},
|
|
1681
|
+
editorState: {
|
|
1682
|
+
selection: {
|
|
1683
|
+
type: "cursor",
|
|
1684
|
+
cursorPosition: {
|
|
1685
|
+
relativePosition: relPosJSON,
|
|
1686
|
+
absoluteOffset: 0
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Update just the cursor offset within the current awareness position.
|
|
1694
|
+
* Used during streaming to move the cursor forward as text is typed.
|
|
1695
|
+
*/
|
|
1696
|
+
updateCursorOffset(offset) {
|
|
1697
|
+
if (!this.awarenessState?.editorState?.selection) return;
|
|
1698
|
+
const selection = this.awarenessState.editorState.selection;
|
|
1699
|
+
if (selection.type === "cursor") {
|
|
1700
|
+
selection.cursorPosition.absoluteOffset = offset;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
// --- Internal ---
|
|
1704
|
+
/**
|
|
1705
|
+
* Assert that the session is in one of the allowed states.
|
|
1706
|
+
*/
|
|
1707
|
+
requireState(...allowed) {
|
|
1708
|
+
if (!allowed.includes(this.state)) {
|
|
1709
|
+
throw new Error(
|
|
1710
|
+
`Operation requires state ${allowed.join(" or ")}, but current state is '${this.state}'`
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
// src/tools/connect.ts
|
|
1719
|
+
import { z } from "zod";
|
|
1720
|
+
function registerConnectTools(server, session) {
|
|
1721
|
+
server.tool(
|
|
1722
|
+
"wp_connect",
|
|
1723
|
+
"Connect to a WordPress site for collaborative editing",
|
|
1724
|
+
{
|
|
1725
|
+
siteUrl: z.string().describe("WordPress site URL (e.g., https://example.com)"),
|
|
1726
|
+
username: z.string().describe("WordPress username"),
|
|
1727
|
+
appPassword: z.string().describe("WordPress Application Password")
|
|
1728
|
+
},
|
|
1729
|
+
async ({ siteUrl, username, appPassword }) => {
|
|
1730
|
+
try {
|
|
1731
|
+
const user = await session.connect({ siteUrl, username, appPassword });
|
|
1732
|
+
return {
|
|
1733
|
+
content: [{ type: "text", text: `Connected to ${siteUrl} as ${user.name} (ID: ${user.id})` }]
|
|
1734
|
+
};
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
return {
|
|
1737
|
+
content: [{ type: "text", text: `Connection failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1738
|
+
isError: true
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
);
|
|
1743
|
+
server.tool(
|
|
1744
|
+
"wp_disconnect",
|
|
1745
|
+
"Disconnect from the WordPress site",
|
|
1746
|
+
{},
|
|
1747
|
+
async () => {
|
|
1748
|
+
session.disconnect();
|
|
1749
|
+
return {
|
|
1750
|
+
content: [{ type: "text", text: "Disconnected from WordPress." }]
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
var init_connect = __esm({
|
|
1756
|
+
"src/tools/connect.ts"() {
|
|
1757
|
+
"use strict";
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
// src/tools/posts.ts
|
|
1762
|
+
import { z as z2 } from "zod";
|
|
1763
|
+
function registerPostTools(server, session) {
|
|
1764
|
+
server.tool(
|
|
1765
|
+
"wp_list_posts",
|
|
1766
|
+
"List WordPress posts with optional filters",
|
|
1767
|
+
{
|
|
1768
|
+
status: z2.string().optional().describe("Filter by post status (e.g., publish, draft, pending)"),
|
|
1769
|
+
search: z2.string().optional().describe("Search posts by keyword"),
|
|
1770
|
+
perPage: z2.number().optional().describe("Number of posts to return (default 10)")
|
|
1771
|
+
},
|
|
1772
|
+
async ({ status, search, perPage }) => {
|
|
1773
|
+
try {
|
|
1774
|
+
const posts = await session.listPosts({ status, search, perPage });
|
|
1775
|
+
if (posts.length === 0) {
|
|
1776
|
+
return {
|
|
1777
|
+
content: [{ type: "text", text: "No posts found." }]
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
const lines = posts.map(
|
|
1781
|
+
(post, i) => `${i + 1}. [${post.id}] ${post.title.raw ?? post.title.rendered} (${post.status})`
|
|
1782
|
+
);
|
|
1783
|
+
return {
|
|
1784
|
+
content: [{
|
|
1785
|
+
type: "text",
|
|
1786
|
+
text: `Found ${posts.length} posts:
|
|
1787
|
+
|
|
1788
|
+
${lines.join("\n")}`
|
|
1789
|
+
}]
|
|
1790
|
+
};
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
return {
|
|
1793
|
+
content: [{ type: "text", text: `Failed to list posts: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1794
|
+
isError: true
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
);
|
|
1799
|
+
server.tool(
|
|
1800
|
+
"wp_open_post",
|
|
1801
|
+
"Open a WordPress post for collaborative editing",
|
|
1802
|
+
{
|
|
1803
|
+
postId: z2.number().describe("The post ID to open")
|
|
1804
|
+
},
|
|
1805
|
+
async ({ postId }) => {
|
|
1806
|
+
try {
|
|
1807
|
+
await session.openPost(postId);
|
|
1808
|
+
const content = session.readPost();
|
|
1809
|
+
return {
|
|
1810
|
+
content: [{
|
|
1811
|
+
type: "text",
|
|
1812
|
+
text: `Opened post ${postId} for editing.
|
|
1813
|
+
|
|
1814
|
+
${content}`
|
|
1815
|
+
}]
|
|
1816
|
+
};
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
return {
|
|
1819
|
+
content: [{ type: "text", text: `Failed to open post: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1820
|
+
isError: true
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
);
|
|
1825
|
+
server.tool(
|
|
1826
|
+
"wp_create_post",
|
|
1827
|
+
"Create a new WordPress post and open it for editing",
|
|
1828
|
+
{
|
|
1829
|
+
title: z2.string().optional().describe("Post title"),
|
|
1830
|
+
content: z2.string().optional().describe("Initial post content (Gutenberg HTML)")
|
|
1831
|
+
},
|
|
1832
|
+
async ({ title, content }) => {
|
|
1833
|
+
try {
|
|
1834
|
+
const post = await session.createPost({ title, content });
|
|
1835
|
+
const rendered = session.readPost();
|
|
1836
|
+
return {
|
|
1837
|
+
content: [{
|
|
1838
|
+
type: "text",
|
|
1839
|
+
text: `Created and opened post ${post.id}.
|
|
1840
|
+
|
|
1841
|
+
${rendered}`
|
|
1842
|
+
}]
|
|
1843
|
+
};
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
return {
|
|
1846
|
+
content: [{ type: "text", text: `Failed to create post: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1847
|
+
isError: true
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
var init_posts = __esm({
|
|
1854
|
+
"src/tools/posts.ts"() {
|
|
1855
|
+
"use strict";
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
// src/tools/read.ts
|
|
1860
|
+
import { z as z3 } from "zod";
|
|
1861
|
+
function registerReadTools(server, session) {
|
|
1862
|
+
server.tool(
|
|
1863
|
+
"wp_read_post",
|
|
1864
|
+
"Read the current post content as a block listing",
|
|
1865
|
+
{},
|
|
1866
|
+
async () => {
|
|
1867
|
+
try {
|
|
1868
|
+
const content = session.readPost();
|
|
1869
|
+
return {
|
|
1870
|
+
content: [{ type: "text", text: content }]
|
|
1871
|
+
};
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
return {
|
|
1874
|
+
content: [{ type: "text", text: `Failed to read post: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1875
|
+
isError: true
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
);
|
|
1880
|
+
server.tool(
|
|
1881
|
+
"wp_read_block",
|
|
1882
|
+
'Read a specific block by index (supports dot notation for nested blocks, e.g., "2.1")',
|
|
1883
|
+
{
|
|
1884
|
+
index: z3.string().describe('Block index (e.g., "0", "2.1" for nested blocks)')
|
|
1885
|
+
},
|
|
1886
|
+
async ({ index }) => {
|
|
1887
|
+
try {
|
|
1888
|
+
const content = session.readBlock(index);
|
|
1889
|
+
return {
|
|
1890
|
+
content: [{ type: "text", text: content }]
|
|
1891
|
+
};
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
return {
|
|
1894
|
+
content: [{ type: "text", text: `Failed to read block: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1895
|
+
isError: true
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
var init_read = __esm({
|
|
1902
|
+
"src/tools/read.ts"() {
|
|
1903
|
+
"use strict";
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
// src/tools/edit.ts
|
|
1908
|
+
import { z as z4 } from "zod";
|
|
1909
|
+
function registerEditTools(server, session) {
|
|
1910
|
+
server.tool(
|
|
1911
|
+
"wp_update_block",
|
|
1912
|
+
"Update a block's content and/or attributes",
|
|
1913
|
+
{
|
|
1914
|
+
index: z4.string().describe('Block index (e.g., "0", "2.1" for nested blocks)'),
|
|
1915
|
+
content: z4.string().optional().describe("New text content for the block"),
|
|
1916
|
+
attributes: z4.record(z4.unknown()).optional().describe("Attributes to update (key-value pairs)")
|
|
1917
|
+
},
|
|
1918
|
+
async ({ index, content, attributes }) => {
|
|
1919
|
+
try {
|
|
1920
|
+
await session.updateBlock(index, { content, attributes });
|
|
1921
|
+
const updated = session.readBlock(index);
|
|
1922
|
+
return {
|
|
1923
|
+
content: [{ type: "text", text: `Updated block ${index}.
|
|
1924
|
+
|
|
1925
|
+
${updated}` }]
|
|
1926
|
+
};
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
return {
|
|
1929
|
+
content: [{ type: "text", text: `Failed to update block: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1930
|
+
isError: true
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
);
|
|
1935
|
+
server.tool(
|
|
1936
|
+
"wp_insert_block",
|
|
1937
|
+
"Insert a new block at a position in the post. Supports nested blocks via innerBlocks.",
|
|
1938
|
+
{
|
|
1939
|
+
position: z4.number().describe("Position to insert the block (0-based index)"),
|
|
1940
|
+
name: z4.string().describe('Block type name (e.g., "core/paragraph", "core/heading", "core/list")'),
|
|
1941
|
+
content: z4.string().optional().describe("Text content for the block"),
|
|
1942
|
+
attributes: z4.record(z4.unknown()).optional().describe("Block attributes (key-value pairs)"),
|
|
1943
|
+
innerBlocks: z4.array(blockInputSchema).optional().describe("Nested child blocks (e.g., list-items inside a list)")
|
|
1944
|
+
},
|
|
1945
|
+
async ({ position, name, content, attributes, innerBlocks }) => {
|
|
1946
|
+
try {
|
|
1947
|
+
await session.insertBlock(position, { name, content, attributes, innerBlocks });
|
|
1948
|
+
return {
|
|
1949
|
+
content: [{ type: "text", text: `Inserted ${name} block at position ${position}.` }]
|
|
1950
|
+
};
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
return {
|
|
1953
|
+
content: [{ type: "text", text: `Failed to insert block: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1954
|
+
isError: true
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
);
|
|
1959
|
+
server.tool(
|
|
1960
|
+
"wp_insert_inner_block",
|
|
1961
|
+
"Insert a block as a child of an existing block (e.g., add a list-item to a list)",
|
|
1962
|
+
{
|
|
1963
|
+
parentIndex: z4.string().describe('Dot-notation index of the parent block (e.g., "0", "2.1")'),
|
|
1964
|
+
position: z4.number().describe("Position within the parent's inner blocks (0-based)"),
|
|
1965
|
+
name: z4.string().describe('Block type name (e.g., "core/list-item")'),
|
|
1966
|
+
content: z4.string().optional().describe("Text content for the block"),
|
|
1967
|
+
attributes: z4.record(z4.unknown()).optional().describe("Block attributes (key-value pairs)"),
|
|
1968
|
+
innerBlocks: z4.array(blockInputSchema).optional().describe("Nested child blocks")
|
|
1969
|
+
},
|
|
1970
|
+
async ({ parentIndex, position, name, content, attributes, innerBlocks }) => {
|
|
1971
|
+
try {
|
|
1972
|
+
await session.insertInnerBlock(parentIndex, position, { name, content, attributes, innerBlocks });
|
|
1973
|
+
return {
|
|
1974
|
+
content: [{ type: "text", text: `Inserted ${name} as inner block at ${parentIndex}.${position}.` }]
|
|
1975
|
+
};
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
return {
|
|
1978
|
+
content: [{ type: "text", text: `Failed to insert inner block: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1979
|
+
isError: true
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
);
|
|
1984
|
+
server.tool(
|
|
1985
|
+
"wp_remove_blocks",
|
|
1986
|
+
"Remove one or more blocks from the post",
|
|
1987
|
+
{
|
|
1988
|
+
startIndex: z4.number().describe("Index of the first block to remove"),
|
|
1989
|
+
count: z4.number().optional().describe("Number of blocks to remove (default 1)")
|
|
1990
|
+
},
|
|
1991
|
+
async ({ startIndex, count }) => {
|
|
1992
|
+
try {
|
|
1993
|
+
const removeCount = count ?? 1;
|
|
1994
|
+
session.removeBlocks(startIndex, removeCount);
|
|
1995
|
+
return {
|
|
1996
|
+
content: [{
|
|
1997
|
+
type: "text",
|
|
1998
|
+
text: `Removed ${removeCount} block${removeCount !== 1 ? "s" : ""} starting at index ${startIndex}.`
|
|
1999
|
+
}]
|
|
2000
|
+
};
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
return {
|
|
2003
|
+
content: [{ type: "text", text: `Failed to remove blocks: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2004
|
+
isError: true
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
);
|
|
2009
|
+
server.tool(
|
|
2010
|
+
"wp_remove_inner_blocks",
|
|
2011
|
+
"Remove inner blocks from a parent block",
|
|
2012
|
+
{
|
|
2013
|
+
parentIndex: z4.string().describe('Dot-notation index of the parent block (e.g., "0")'),
|
|
2014
|
+
startIndex: z4.number().describe("Index of the first inner block to remove"),
|
|
2015
|
+
count: z4.number().optional().describe("Number of inner blocks to remove (default 1)")
|
|
2016
|
+
},
|
|
2017
|
+
async ({ parentIndex, startIndex, count }) => {
|
|
2018
|
+
try {
|
|
2019
|
+
const removeCount = count ?? 1;
|
|
2020
|
+
session.removeInnerBlocks(parentIndex, startIndex, removeCount);
|
|
2021
|
+
return {
|
|
2022
|
+
content: [{
|
|
2023
|
+
type: "text",
|
|
2024
|
+
text: `Removed ${removeCount} inner block${removeCount !== 1 ? "s" : ""} from block ${parentIndex}.`
|
|
2025
|
+
}]
|
|
2026
|
+
};
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
return {
|
|
2029
|
+
content: [{ type: "text", text: `Failed to remove inner blocks: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2030
|
+
isError: true
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
);
|
|
2035
|
+
server.tool(
|
|
2036
|
+
"wp_move_block",
|
|
2037
|
+
"Move a block from one position to another",
|
|
2038
|
+
{
|
|
2039
|
+
fromIndex: z4.number().describe("Current position of the block"),
|
|
2040
|
+
toIndex: z4.number().describe("Target position for the block")
|
|
2041
|
+
},
|
|
2042
|
+
async ({ fromIndex, toIndex }) => {
|
|
2043
|
+
try {
|
|
2044
|
+
session.moveBlock(fromIndex, toIndex);
|
|
2045
|
+
return {
|
|
2046
|
+
content: [{
|
|
2047
|
+
type: "text",
|
|
2048
|
+
text: `Moved block from position ${fromIndex} to ${toIndex}.`
|
|
2049
|
+
}]
|
|
2050
|
+
};
|
|
2051
|
+
} catch (error) {
|
|
2052
|
+
return {
|
|
2053
|
+
content: [{ type: "text", text: `Failed to move block: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2054
|
+
isError: true
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
);
|
|
2059
|
+
server.tool(
|
|
2060
|
+
"wp_replace_blocks",
|
|
2061
|
+
"Replace a range of blocks with new blocks. Supports nested blocks via innerBlocks.",
|
|
2062
|
+
{
|
|
2063
|
+
startIndex: z4.number().describe("Index of the first block to replace"),
|
|
2064
|
+
count: z4.number().describe("Number of blocks to replace"),
|
|
2065
|
+
blocks: z4.array(blockInputSchema).describe("New blocks to insert in place of the removed ones")
|
|
2066
|
+
},
|
|
2067
|
+
async ({ startIndex, count, blocks }) => {
|
|
2068
|
+
try {
|
|
2069
|
+
await session.replaceBlocks(startIndex, count, blocks);
|
|
2070
|
+
return {
|
|
2071
|
+
content: [{
|
|
2072
|
+
type: "text",
|
|
2073
|
+
text: `Replaced ${count} block${count !== 1 ? "s" : ""} at index ${startIndex} with ${blocks.length} new block${blocks.length !== 1 ? "s" : ""}.`
|
|
2074
|
+
}]
|
|
2075
|
+
};
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
return {
|
|
2078
|
+
content: [{ type: "text", text: `Failed to replace blocks: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2079
|
+
isError: true
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
);
|
|
2084
|
+
server.tool(
|
|
2085
|
+
"wp_set_title",
|
|
2086
|
+
"Set the post title",
|
|
2087
|
+
{
|
|
2088
|
+
title: z4.string().describe("New post title")
|
|
2089
|
+
},
|
|
2090
|
+
async ({ title }) => {
|
|
2091
|
+
try {
|
|
2092
|
+
await session.setTitle(title);
|
|
2093
|
+
return {
|
|
2094
|
+
content: [{ type: "text", text: `Title set to "${title}".` }]
|
|
2095
|
+
};
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
return {
|
|
2098
|
+
content: [{ type: "text", text: `Failed to set title: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2099
|
+
isError: true
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
var blockInputSchema;
|
|
2106
|
+
var init_edit = __esm({
|
|
2107
|
+
"src/tools/edit.ts"() {
|
|
2108
|
+
"use strict";
|
|
2109
|
+
blockInputSchema = z4.object({
|
|
2110
|
+
name: z4.string().describe('Block type name (e.g., "core/paragraph", "core/list-item")'),
|
|
2111
|
+
content: z4.string().optional().describe("Text content for the block"),
|
|
2112
|
+
attributes: z4.record(z4.unknown()).optional().describe("Block attributes (key-value pairs)"),
|
|
2113
|
+
innerBlocks: z4.lazy(() => z4.array(blockInputSchema)).optional().describe("Nested child blocks (e.g., list-items inside a list)")
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
// src/tools/status.ts
|
|
2119
|
+
function registerStatusTools(server, session) {
|
|
2120
|
+
server.tool(
|
|
2121
|
+
"wp_status",
|
|
2122
|
+
"Show current connection state, sync status, and post info",
|
|
2123
|
+
{},
|
|
2124
|
+
async () => {
|
|
2125
|
+
const state = session.getState();
|
|
2126
|
+
const lines = [];
|
|
2127
|
+
if (state === "disconnected") {
|
|
2128
|
+
lines.push("Connection: disconnected");
|
|
2129
|
+
lines.push("");
|
|
2130
|
+
lines.push("Use wp_connect to connect to a WordPress site.");
|
|
2131
|
+
} else {
|
|
2132
|
+
const user = session.getUser();
|
|
2133
|
+
lines.push("Connection: connected");
|
|
2134
|
+
lines.push(`User: ${user?.name ?? "unknown"} (ID: ${user?.id ?? "?"})`);
|
|
2135
|
+
const post = session.getCurrentPost();
|
|
2136
|
+
if (state === "editing" && post) {
|
|
2137
|
+
const syncStatus = session.getSyncStatus();
|
|
2138
|
+
const collaboratorCount = session.getCollaborators().length;
|
|
2139
|
+
lines.push(`Sync: ${syncStatus?.isPolling ? "polling" : "stopped"} (${collaboratorCount + 1} collaborator${collaboratorCount + 1 !== 1 ? "s" : ""})`);
|
|
2140
|
+
lines.push(`Post: "${post.title.raw ?? post.title.rendered}" (ID: ${post.id}, status: ${post.status})`);
|
|
2141
|
+
lines.push(`Queue: ${syncStatus?.queueSize ?? 0} pending updates`);
|
|
2142
|
+
} else {
|
|
2143
|
+
lines.push("Post: none open");
|
|
2144
|
+
lines.push("");
|
|
2145
|
+
lines.push("Use wp_open_post or wp_create_post to start editing.");
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return {
|
|
2149
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
);
|
|
2153
|
+
server.tool(
|
|
2154
|
+
"wp_collaborators",
|
|
2155
|
+
"List active collaborators on the current post",
|
|
2156
|
+
{},
|
|
2157
|
+
async () => {
|
|
2158
|
+
try {
|
|
2159
|
+
const state = session.getState();
|
|
2160
|
+
if (state !== "editing") {
|
|
2161
|
+
return {
|
|
2162
|
+
content: [{ type: "text", text: "No post is currently open for editing." }],
|
|
2163
|
+
isError: true
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
const collaborators = session.getCollaborators();
|
|
2167
|
+
const user = session.getUser();
|
|
2168
|
+
const lines = ["Active collaborators:"];
|
|
2169
|
+
if (user) {
|
|
2170
|
+
lines.push(`- ${user.name} (AI, Claude Code MCP)`);
|
|
2171
|
+
}
|
|
2172
|
+
for (const collab of collaborators) {
|
|
2173
|
+
lines.push(`- ${collab.name} (Human, ${collab.browserType})`);
|
|
2174
|
+
}
|
|
2175
|
+
if (collaborators.length === 0 && !user) {
|
|
2176
|
+
lines.push("- No collaborators detected");
|
|
2177
|
+
}
|
|
2178
|
+
return {
|
|
2179
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
2180
|
+
};
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
return {
|
|
2183
|
+
content: [{ type: "text", text: `Failed to get collaborators: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2184
|
+
isError: true
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
);
|
|
2189
|
+
server.tool(
|
|
2190
|
+
"wp_save",
|
|
2191
|
+
"Save the current post",
|
|
2192
|
+
{},
|
|
2193
|
+
async () => {
|
|
2194
|
+
try {
|
|
2195
|
+
session.save();
|
|
2196
|
+
const post = session.getCurrentPost();
|
|
2197
|
+
return {
|
|
2198
|
+
content: [{
|
|
2199
|
+
type: "text",
|
|
2200
|
+
text: `Post "${post?.title.raw ?? post?.title.rendered ?? "Untitled"}" saved.`
|
|
2201
|
+
}]
|
|
2202
|
+
};
|
|
2203
|
+
} catch (error) {
|
|
2204
|
+
return {
|
|
2205
|
+
content: [{ type: "text", text: `Failed to save: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2206
|
+
isError: true
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
var init_status = __esm({
|
|
2213
|
+
"src/tools/status.ts"() {
|
|
2214
|
+
"use strict";
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
// src/server.ts
|
|
2219
|
+
var server_exports = {};
|
|
2220
|
+
__export(server_exports, {
|
|
2221
|
+
VERSION: () => VERSION,
|
|
2222
|
+
startServer: () => startServer
|
|
2223
|
+
});
|
|
2224
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2225
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2226
|
+
async function startServer() {
|
|
2227
|
+
const server = new McpServer({
|
|
2228
|
+
name: "claudaborative-editing",
|
|
2229
|
+
version: VERSION
|
|
2230
|
+
});
|
|
2231
|
+
const session = new SessionManager();
|
|
2232
|
+
const siteUrl = process.env.WP_SITE_URL;
|
|
2233
|
+
const username = process.env.WP_USERNAME;
|
|
2234
|
+
const appPassword = process.env.WP_APP_PASSWORD;
|
|
2235
|
+
if (siteUrl && username && appPassword) {
|
|
2236
|
+
try {
|
|
2237
|
+
await session.connect({ siteUrl, username, appPassword });
|
|
2238
|
+
} catch (e) {
|
|
2239
|
+
console.error("Auto-connect failed:", e);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
registerConnectTools(server, session);
|
|
2243
|
+
registerPostTools(server, session);
|
|
2244
|
+
registerReadTools(server, session);
|
|
2245
|
+
registerEditTools(server, session);
|
|
2246
|
+
registerStatusTools(server, session);
|
|
2247
|
+
const transport = new StdioServerTransport();
|
|
2248
|
+
await server.connect(transport);
|
|
2249
|
+
}
|
|
2250
|
+
var VERSION;
|
|
2251
|
+
var init_server = __esm({
|
|
2252
|
+
"src/server.ts"() {
|
|
2253
|
+
"use strict";
|
|
2254
|
+
init_session_manager();
|
|
2255
|
+
init_connect();
|
|
2256
|
+
init_posts();
|
|
2257
|
+
init_read();
|
|
2258
|
+
init_edit();
|
|
2259
|
+
init_status();
|
|
2260
|
+
VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
2261
|
+
}
|
|
2262
|
+
});
|
|
2263
|
+
|
|
2264
|
+
// src/cli/setup.ts
|
|
2265
|
+
var setup_exports = {};
|
|
2266
|
+
__export(setup_exports, {
|
|
2267
|
+
runSetup: () => runSetup,
|
|
2268
|
+
shellQuote: () => shellQuote
|
|
2269
|
+
});
|
|
2270
|
+
import { createInterface } from "readline";
|
|
2271
|
+
function defaultDeps() {
|
|
2272
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2273
|
+
return {
|
|
2274
|
+
prompt: (question) => new Promise((resolve) => {
|
|
2275
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
2276
|
+
}),
|
|
2277
|
+
log: (msg) => console.log(msg),
|
|
2278
|
+
error: (msg) => console.error(`Error: ${msg}`),
|
|
2279
|
+
exit: (code) => {
|
|
2280
|
+
rl.close();
|
|
2281
|
+
return process.exit(code);
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
async function runSetup(deps = defaultDeps()) {
|
|
2286
|
+
const { prompt, log, error, exit } = deps;
|
|
2287
|
+
log("");
|
|
2288
|
+
log("claudaborative-editing setup");
|
|
2289
|
+
log("============================");
|
|
2290
|
+
log("");
|
|
2291
|
+
log("This wizard will validate your WordPress credentials and give you");
|
|
2292
|
+
log("the command to register this MCP server with Claude Code.");
|
|
2293
|
+
log("");
|
|
2294
|
+
log("Prerequisites:");
|
|
2295
|
+
log(" - WordPress 7.0+ with collaborative editing enabled");
|
|
2296
|
+
log(" (Settings \u2192 Writing in your WordPress admin)");
|
|
2297
|
+
log(" - An Application Password for your WordPress user");
|
|
2298
|
+
log(" (Users \u2192 Your Profile \u2192 Application Passwords)");
|
|
2299
|
+
log("");
|
|
2300
|
+
const siteUrl = await prompt("WordPress site URL: ");
|
|
2301
|
+
if (!siteUrl) {
|
|
2302
|
+
error("Site URL is required.");
|
|
2303
|
+
exit(1);
|
|
2304
|
+
}
|
|
2305
|
+
const username = await prompt("WordPress username: ");
|
|
2306
|
+
if (!username) {
|
|
2307
|
+
error("Username is required.");
|
|
2308
|
+
exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
const appPassword = await prompt("Application Password: ");
|
|
2311
|
+
if (!appPassword) {
|
|
2312
|
+
error("Application Password is required.");
|
|
2313
|
+
exit(1);
|
|
2314
|
+
}
|
|
2315
|
+
log("");
|
|
2316
|
+
log("Validating credentials...");
|
|
2317
|
+
const client = new WordPressApiClient({
|
|
2318
|
+
siteUrl,
|
|
2319
|
+
username,
|
|
2320
|
+
appPassword
|
|
2321
|
+
});
|
|
2322
|
+
let displayName;
|
|
2323
|
+
try {
|
|
2324
|
+
const user = await client.validateConnection();
|
|
2325
|
+
displayName = user.name ?? username;
|
|
2326
|
+
log(` \u2713 Authenticated as "${displayName}"`);
|
|
2327
|
+
} catch (err) {
|
|
2328
|
+
if (err instanceof WordPressApiError) {
|
|
2329
|
+
error(err.message);
|
|
2330
|
+
} else {
|
|
2331
|
+
error(`Could not connect to ${siteUrl}. Check the URL and try again.`);
|
|
2332
|
+
}
|
|
2333
|
+
exit(1);
|
|
2334
|
+
}
|
|
2335
|
+
try {
|
|
2336
|
+
await client.validateSyncEndpoint();
|
|
2337
|
+
log(" \u2713 Collaborative editing endpoint available");
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
if (err instanceof WordPressApiError && err.status === 404) {
|
|
2340
|
+
log("");
|
|
2341
|
+
error(
|
|
2342
|
+
"Collaborative editing is not enabled.\n Go to Settings \u2192 Writing in your WordPress admin and enable it.\n (Requires WordPress 7.0 or later.)"
|
|
2343
|
+
);
|
|
2344
|
+
exit(1);
|
|
2345
|
+
}
|
|
2346
|
+
if (err instanceof WordPressApiError) {
|
|
2347
|
+
error(err.message);
|
|
2348
|
+
} else {
|
|
2349
|
+
error("Could not validate the sync endpoint.");
|
|
2350
|
+
}
|
|
2351
|
+
exit(1);
|
|
2352
|
+
}
|
|
2353
|
+
log("");
|
|
2354
|
+
log("Setup complete! Run this command to register the MCP server:");
|
|
2355
|
+
log("");
|
|
2356
|
+
const envFlags = [
|
|
2357
|
+
`-e WP_SITE_URL=${shellQuote(siteUrl)}`,
|
|
2358
|
+
`-e WP_USERNAME=${shellQuote(username)}`,
|
|
2359
|
+
`-e WP_APP_PASSWORD=${shellQuote(appPassword)}`
|
|
2360
|
+
].join(" ");
|
|
2361
|
+
log(` claude mcp add claudaborative-editing ${envFlags} -- npx claudaborative-editing`);
|
|
2362
|
+
log("");
|
|
2363
|
+
}
|
|
2364
|
+
function shellQuote(value) {
|
|
2365
|
+
if (/^[a-zA-Z0-9_./:@-]+$/.test(value)) {
|
|
2366
|
+
return value;
|
|
2367
|
+
}
|
|
2368
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
|
|
2369
|
+
return `"${escaped}"`;
|
|
2370
|
+
}
|
|
2371
|
+
var init_setup = __esm({
|
|
2372
|
+
"src/cli/setup.ts"() {
|
|
2373
|
+
"use strict";
|
|
2374
|
+
init_api_client();
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
// src/index.ts
|
|
2379
|
+
init_server();
|
|
2380
|
+
var args = process.argv.slice(2);
|
|
2381
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
2382
|
+
console.log(VERSION);
|
|
2383
|
+
process.exit(0);
|
|
2384
|
+
}
|
|
2385
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2386
|
+
console.log(`claudaborative-editing v${VERSION}
|
|
2387
|
+
|
|
2388
|
+
MCP server for collaborative WordPress post editing via Yjs CRDT.
|
|
2389
|
+
|
|
2390
|
+
Usage:
|
|
2391
|
+
claudaborative-editing Start the MCP server (stdio transport)
|
|
2392
|
+
claudaborative-editing setup Interactive setup wizard
|
|
2393
|
+
claudaborative-editing --version Print version
|
|
2394
|
+
claudaborative-editing --help Show this help
|
|
2395
|
+
|
|
2396
|
+
Environment variables:
|
|
2397
|
+
WP_SITE_URL WordPress site URL
|
|
2398
|
+
WP_USERNAME WordPress username
|
|
2399
|
+
WP_APP_PASSWORD WordPress Application Password
|
|
2400
|
+
|
|
2401
|
+
More info: https://github.com/pento/claudaborative-editing`);
|
|
2402
|
+
process.exit(0);
|
|
2403
|
+
}
|
|
2404
|
+
if (args[0] === "setup") {
|
|
2405
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
2406
|
+
await runSetup2();
|
|
2407
|
+
} else {
|
|
2408
|
+
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
2409
|
+
startServer2().catch((error) => {
|
|
2410
|
+
console.error("Failed to start server:", error);
|
|
2411
|
+
process.exit(1);
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
//# sourceMappingURL=index.js.map
|