fantsec-docmost-cli 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/build/__tests__/cli-utils.test.js +287 -0
  4. package/build/__tests__/client-pagination.test.js +103 -0
  5. package/build/__tests__/discovery.test.js +40 -0
  6. package/build/__tests__/envelope.test.js +91 -0
  7. package/build/__tests__/filters.test.js +235 -0
  8. package/build/__tests__/integration/comment.test.js +48 -0
  9. package/build/__tests__/integration/discovery.test.js +24 -0
  10. package/build/__tests__/integration/file.test.js +33 -0
  11. package/build/__tests__/integration/group.test.js +48 -0
  12. package/build/__tests__/integration/helpers/global-setup.js +80 -0
  13. package/build/__tests__/integration/helpers/run-cli.js +163 -0
  14. package/build/__tests__/integration/invite.test.js +34 -0
  15. package/build/__tests__/integration/page.test.js +69 -0
  16. package/build/__tests__/integration/search.test.js +45 -0
  17. package/build/__tests__/integration/share.test.js +49 -0
  18. package/build/__tests__/integration/space.test.js +56 -0
  19. package/build/__tests__/integration/user.test.js +15 -0
  20. package/build/__tests__/integration/workspace.test.js +42 -0
  21. package/build/__tests__/markdown-converter.test.js +445 -0
  22. package/build/__tests__/mcp-tooling.test.js +58 -0
  23. package/build/__tests__/page-mentions.test.js +65 -0
  24. package/build/__tests__/tiptap-extensions.test.js +135 -0
  25. package/build/client.js +715 -0
  26. package/build/commands/comment.js +54 -0
  27. package/build/commands/discovery.js +21 -0
  28. package/build/commands/file.js +36 -0
  29. package/build/commands/group.js +91 -0
  30. package/build/commands/invite.js +67 -0
  31. package/build/commands/page.js +227 -0
  32. package/build/commands/search.js +33 -0
  33. package/build/commands/share.js +65 -0
  34. package/build/commands/space.js +154 -0
  35. package/build/commands/user.js +38 -0
  36. package/build/commands/workspace.js +77 -0
  37. package/build/index.js +19 -0
  38. package/build/lib/auth-utils.js +53 -0
  39. package/build/lib/cli-utils.js +293 -0
  40. package/build/lib/collaboration.js +126 -0
  41. package/build/lib/filters.js +137 -0
  42. package/build/lib/markdown-converter.js +187 -0
  43. package/build/lib/mcp-tooling.js +295 -0
  44. package/build/lib/page-mentions.js +162 -0
  45. package/build/lib/tiptap-extensions.js +86 -0
  46. package/build/mcp.js +186 -0
  47. package/build/program.js +60 -0
  48. package/package.json +64 -0
@@ -0,0 +1,126 @@
1
+ import { HocuspocusProvider } from "@hocuspocus/provider";
2
+ import { TiptapTransformer } from "@hocuspocus/transformer";
3
+ import * as Y from "yjs";
4
+ import WebSocket from "ws";
5
+ import { JSDOM } from "jsdom";
6
+ import { tiptapExtensions } from "./tiptap-extensions.js";
7
+ const debug = (...args) => {
8
+ if (process.env.DEBUG)
9
+ console.error(...args);
10
+ };
11
+ let domSetup = false;
12
+ function setupDomEnvironment() {
13
+ if (domSetup)
14
+ return;
15
+ const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
16
+ global.window = dom.window;
17
+ global.document = dom.window.document;
18
+ // @ts-ignore
19
+ global.Element = dom.window.Element;
20
+ // @ts-ignore
21
+ global.WebSocket = WebSocket;
22
+ domSetup = true;
23
+ }
24
+ export async function updatePageContentRealtime(pageId, tiptapJson, collabToken, baseUrl) {
25
+ setupDomEnvironment();
26
+ debug(`Starting realtime update for page ${pageId}`);
27
+ debug(`Collab token: ${collabToken ? "present" : "missing"}`);
28
+ // 1. Setup Hocuspocus Provider
29
+ const ydoc = new Y.Doc();
30
+ // Construct WebSocket URL
31
+ // Replace protocol
32
+ let wsUrl = baseUrl.replace(/^http/, "ws");
33
+ const urlObj = new URL(wsUrl);
34
+ // Remove /api suffix if present, as the websocket is mounted on root /collab
35
+ if (urlObj.pathname.endsWith("/api") || urlObj.pathname.endsWith("/api/")) {
36
+ urlObj.pathname = urlObj.pathname.replace(/\/api\/?$/, "");
37
+ }
38
+ // Set correct path to /collab
39
+ urlObj.pathname = urlObj.pathname.replace(/\/$/, "") + "/collab";
40
+ wsUrl = urlObj.toString();
41
+ debug(`Connecting to WebSocket: ${wsUrl}`);
42
+ return new Promise((resolve, reject) => {
43
+ let synced = false;
44
+ let settled = false;
45
+ const fail = (error) => {
46
+ if (settled)
47
+ return;
48
+ settled = true;
49
+ clearTimeout(timer);
50
+ reject(error);
51
+ };
52
+ // Safety timeout
53
+ const timer = setTimeout(() => {
54
+ if (provider)
55
+ provider.destroy();
56
+ fail(new Error("Connection timeout to collaboration server"));
57
+ }, 25000);
58
+ const provider = new HocuspocusProvider({
59
+ url: wsUrl,
60
+ name: `page.${pageId}`,
61
+ document: ydoc,
62
+ token: collabToken,
63
+ // @ts-ignore - Required for Node.js environment
64
+ WebSocketPolyfill: WebSocket,
65
+ onConnect: () => debug("WS Connect"),
66
+ onDisconnect: () => {
67
+ debug("WS Disconnect");
68
+ if (!synced) {
69
+ provider.destroy();
70
+ fail(new Error("WebSocket disconnected before sync completed"));
71
+ }
72
+ },
73
+ onClose: () => {
74
+ debug("WS Close");
75
+ if (!synced) {
76
+ fail(new Error("WebSocket closed before sync completed"));
77
+ }
78
+ },
79
+ onSynced: () => {
80
+ synced = true;
81
+ debug("Connected and synced!");
82
+ try {
83
+ // Prepare the new content in a separate doc
84
+ const tempDoc = TiptapTransformer.toYdoc(tiptapJson, "default", tiptapExtensions);
85
+ // Clear existing content
86
+ ydoc.transact(() => {
87
+ const fragment = ydoc.getXmlFragment("default");
88
+ if (fragment.length > 0) {
89
+ fragment.delete(0, fragment.length);
90
+ }
91
+ });
92
+ // Apply new content from tempDoc (outside transact to avoid nested transactions)
93
+ const update = Y.encodeStateAsUpdate(tempDoc);
94
+ Y.applyUpdate(ydoc, update);
95
+ debug("Content replaced. Background persistence in progress (server saves after ~10s debounce)...");
96
+ // Clear safety timeout as we are successful
97
+ clearTimeout(timer);
98
+ settled = true;
99
+ // Resolve immediately so the user doesn't have to wait
100
+ resolve();
101
+ // Keep connection open in background for save/sync (Docmost has 10s debounce)
102
+ // The node process will keep running this timeout even after the tool returns
103
+ const bgTimer = setTimeout(() => {
104
+ try {
105
+ debug(`Closing background connection for page ${pageId}`);
106
+ provider.destroy();
107
+ }
108
+ catch (err) {
109
+ const msg = err instanceof Error ? err.message : String(err);
110
+ process.stderr.write(`Warning: failed to close WebSocket: ${msg}\n`);
111
+ }
112
+ }, 15000);
113
+ bgTimer.unref();
114
+ }
115
+ catch (e) {
116
+ provider.destroy();
117
+ fail(e instanceof Error ? e : new Error(String(e)));
118
+ }
119
+ },
120
+ onAuthenticationFailed: () => {
121
+ provider.destroy();
122
+ fail(new Error("Authentication failed for collaboration connection"));
123
+ },
124
+ });
125
+ });
126
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Filter functions to extract only relevant information from API responses
3
+ * for better agent consumption
4
+ */
5
+ export function filterWorkspace(data) {
6
+ return {
7
+ id: data.id,
8
+ name: data.name,
9
+ description: data.description,
10
+ defaultSpaceId: data.defaultSpaceId,
11
+ createdAt: data.createdAt,
12
+ updatedAt: data.updatedAt,
13
+ deletedAt: data.deletedAt,
14
+ };
15
+ }
16
+ export function filterSpace(space) {
17
+ return {
18
+ id: space.id,
19
+ name: space.name,
20
+ description: space.description,
21
+ slug: space.slug,
22
+ visibility: space.visibility,
23
+ createdAt: space.createdAt,
24
+ updatedAt: space.updatedAt,
25
+ deletedAt: space.deletedAt,
26
+ };
27
+ }
28
+ export function filterGroup(group) {
29
+ return {
30
+ id: group.id,
31
+ name: group.name,
32
+ description: group.description,
33
+ workspaceId: group.workspaceId,
34
+ createdAt: group.createdAt,
35
+ updatedAt: group.updatedAt,
36
+ deletedAt: group.deletedAt,
37
+ };
38
+ }
39
+ export function filterPage(page, content, subpages) {
40
+ return {
41
+ id: page.id,
42
+ title: page.title,
43
+ parentPageId: page.parentPageId,
44
+ spaceId: page.spaceId,
45
+ isLocked: page.isLocked,
46
+ createdAt: page.createdAt,
47
+ updatedAt: page.updatedAt,
48
+ deletedAt: page.deletedAt,
49
+ // Include converted markdown content if valid string (even empty)
50
+ ...(typeof content === "string" && { content }),
51
+ // Include subpages if provided
52
+ ...(subpages &&
53
+ subpages.length > 0 && {
54
+ subpages: subpages.map((p) => ({ id: p.id, title: p.title })),
55
+ }),
56
+ };
57
+ }
58
+ export function filterSearchResult(result) {
59
+ return {
60
+ id: result.id,
61
+ title: result.title,
62
+ parentPageId: result.parentPageId,
63
+ createdAt: result.createdAt,
64
+ updatedAt: result.updatedAt,
65
+ rank: result.rank,
66
+ highlight: result.highlight,
67
+ spaceId: result.space?.id,
68
+ spaceName: result.space?.name,
69
+ };
70
+ }
71
+ export function filterHistoryEntry(entry) {
72
+ return {
73
+ id: entry.id,
74
+ pageId: entry.pageId,
75
+ title: entry.title,
76
+ version: entry.version,
77
+ createdAt: entry.createdAt,
78
+ lastUpdatedBy: entry.lastUpdatedBy?.name || entry.lastUpdatedById,
79
+ contributors: entry.contributors?.map((c) => c.name) || [],
80
+ };
81
+ }
82
+ export function filterMember(member) {
83
+ return {
84
+ id: member.id,
85
+ name: member.name,
86
+ email: member.email,
87
+ role: member.role,
88
+ createdAt: member.createdAt,
89
+ };
90
+ }
91
+ export function filterInvite(invite) {
92
+ return {
93
+ id: invite.id,
94
+ email: invite.email,
95
+ role: invite.role,
96
+ status: invite.status,
97
+ invitedById: invite.invitedById,
98
+ createdAt: invite.createdAt,
99
+ };
100
+ }
101
+ export function filterUser(user) {
102
+ return {
103
+ id: user.id,
104
+ name: user.name,
105
+ email: user.email,
106
+ role: user.role,
107
+ locale: user.locale,
108
+ createdAt: user.createdAt,
109
+ };
110
+ }
111
+ export function filterComment(comment) {
112
+ return {
113
+ id: comment.id,
114
+ pageId: comment.pageId,
115
+ content: comment.content,
116
+ selection: comment.selection,
117
+ parentCommentId: comment.parentCommentId,
118
+ creatorId: comment.creatorId,
119
+ createdAt: comment.createdAt,
120
+ updatedAt: comment.updatedAt,
121
+ };
122
+ }
123
+ export function filterShare(share) {
124
+ return {
125
+ id: share.id,
126
+ pageId: share.pageId,
127
+ includeSubPages: share.includeSubPages,
128
+ searchIndexing: share.searchIndexing,
129
+ createdAt: share.createdAt,
130
+ };
131
+ }
132
+ export function filterHistoryDetail(entry, content) {
133
+ return {
134
+ ...filterHistoryEntry(entry),
135
+ ...(typeof content === "string" && { content }),
136
+ };
137
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Convert ProseMirror/TipTap JSON content to Markdown
3
+ * Supports all Docmost-specific node types and extensions
4
+ */
5
+ function escapeHtmlAttr(value) {
6
+ return value
7
+ .replace(/&/g, "&amp;")
8
+ .replace(/"/g, "&quot;")
9
+ .replace(/'/g, "&#39;")
10
+ .replace(/</g, "&lt;")
11
+ .replace(/>/g, "&gt;");
12
+ }
13
+ function sanitizeUrl(url) {
14
+ const trimmed = url.trim();
15
+ if (/^javascript:/i.test(trimmed) || /^data:/i.test(trimmed) || /^vbscript:/i.test(trimmed)) {
16
+ return "";
17
+ }
18
+ return trimmed;
19
+ }
20
+ export function convertProseMirrorToMarkdown(content) {
21
+ if (!content || !content.content)
22
+ return "";
23
+ const processNode = (node) => {
24
+ const type = node.type;
25
+ const nodeContent = node.content || [];
26
+ switch (type) {
27
+ case "doc":
28
+ return nodeContent.map(processNode).join("\n\n");
29
+ case "paragraph":
30
+ const text = nodeContent.map(processNode).join("");
31
+ const align = node.attrs?.textAlign;
32
+ if (align && align !== "left") {
33
+ return `<div align="${escapeHtmlAttr(align)}">${text}</div>`;
34
+ }
35
+ return text || "";
36
+ case "heading":
37
+ const level = node.attrs?.level || 1;
38
+ const headingText = nodeContent.map(processNode).join("");
39
+ return "#".repeat(level) + " " + headingText;
40
+ case "text":
41
+ let textContent = node.text || "";
42
+ // Apply marks (bold, italic, code, etc.)
43
+ if (node.marks) {
44
+ for (const mark of node.marks) {
45
+ switch (mark.type) {
46
+ case "bold":
47
+ textContent = `**${textContent}**`;
48
+ break;
49
+ case "italic":
50
+ textContent = `*${textContent}*`;
51
+ break;
52
+ case "code":
53
+ textContent = `\`${textContent}\``;
54
+ break;
55
+ case "link":
56
+ textContent = `[${textContent}](${sanitizeUrl(mark.attrs?.href || "")})`;
57
+ break;
58
+ case "strike":
59
+ textContent = `~~${textContent}~~`;
60
+ break;
61
+ case "underline":
62
+ textContent = `<u>${textContent}</u>`;
63
+ break;
64
+ case "subscript":
65
+ textContent = `<sub>${textContent}</sub>`;
66
+ break;
67
+ case "superscript":
68
+ textContent = `<sup>${textContent}</sup>`;
69
+ break;
70
+ case "highlight":
71
+ const color = mark.attrs?.color || "yellow";
72
+ textContent = `<mark style="background-color: ${escapeHtmlAttr(color)}">${textContent}</mark>`;
73
+ break;
74
+ case "textStyle":
75
+ if (mark.attrs?.color) {
76
+ textContent = `<span style="color: ${escapeHtmlAttr(mark.attrs.color)}">${textContent}</span>`;
77
+ }
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ return textContent;
83
+ case "codeBlock":
84
+ const language = node.attrs?.language || "";
85
+ const code = nodeContent.map(processNode).join("");
86
+ return "```" + language + "\n" + code + "\n```";
87
+ case "bulletList":
88
+ return nodeContent
89
+ .map((item) => processListItem(item, "-"))
90
+ .join("\n");
91
+ case "orderedList":
92
+ return nodeContent
93
+ .map((item, index) => processListItem(item, `${index + 1}.`))
94
+ .join("\n");
95
+ case "taskList":
96
+ return nodeContent.map((item) => processTaskItem(item)).join("\n");
97
+ case "taskItem":
98
+ const checked = node.attrs?.checked || false;
99
+ const checkbox = checked ? "[x]" : "[ ]";
100
+ return `- ${checkbox} ${nodeContent.map(processNode).join("\n")}`;
101
+ case "listItem":
102
+ return nodeContent.map(processNode).join("\n");
103
+ case "blockquote":
104
+ return nodeContent.map((n) => "> " + processNode(n)).join("\n");
105
+ case "horizontalRule":
106
+ return "---";
107
+ case "hardBreak":
108
+ return "\n";
109
+ case "image":
110
+ const imgAlt = node.attrs?.alt || "";
111
+ const imgSrc = sanitizeUrl(node.attrs?.src || "");
112
+ const imgCaption = node.attrs?.caption || "";
113
+ return `![${imgAlt}](${imgSrc})${imgCaption ? `\n*${imgCaption}*` : ""}`;
114
+ case "video":
115
+ const videoSrc = sanitizeUrl(node.attrs?.src || "");
116
+ return `🎥 [Video](${videoSrc})`;
117
+ case "youtube":
118
+ const youtubeUrl = sanitizeUrl(node.attrs?.src || "");
119
+ return `📺 [YouTube Video](${youtubeUrl})`;
120
+ case "table":
121
+ const rows = nodeContent.map(processNode);
122
+ if (rows.length > 0) {
123
+ const colCount = (nodeContent[0]?.content || []).length || 1;
124
+ const separator = "|" + " --- |".repeat(colCount);
125
+ rows.splice(1, 0, separator);
126
+ }
127
+ return rows.join("\n");
128
+ case "tableRow":
129
+ return "| " + nodeContent.map(processNode).join(" | ") + " |";
130
+ case "tableCell":
131
+ case "tableHeader":
132
+ return nodeContent.map(processNode).join("");
133
+ case "callout":
134
+ const calloutType = node.attrs?.type || "info";
135
+ const calloutContent = nodeContent.map(processNode).join("\n");
136
+ return `:::${calloutType.toLowerCase()}\n${calloutContent}\n:::`;
137
+ case "details":
138
+ return nodeContent.map(processNode).join("\n");
139
+ case "detailsSummary":
140
+ const summaryText = nodeContent.map(processNode).join("");
141
+ return `<details>\n<summary>${summaryText}</summary>\n`;
142
+ case "detailsContent":
143
+ const detailsText = nodeContent.map(processNode).join("\n");
144
+ return `${detailsText}\n</details>`;
145
+ case "mathInline":
146
+ const inlineMath = node.attrs?.text || "";
147
+ return `$${inlineMath}$`;
148
+ case "mathBlock":
149
+ const blockMath = node.attrs?.text || "";
150
+ return `$$\n${blockMath}\n$$`;
151
+ case "mention":
152
+ const mentionLabel = node.attrs?.label || node.attrs?.id || "";
153
+ return `@${mentionLabel}`;
154
+ case "attachment":
155
+ const attachmentName = node.attrs?.fileName || "attachment";
156
+ const attachmentUrl = sanitizeUrl(node.attrs?.src || "");
157
+ return `📎 [${attachmentName}](${attachmentUrl})`;
158
+ case "drawio":
159
+ return `📊 [Draw.io Diagram]`;
160
+ case "excalidraw":
161
+ return `✏️ [Excalidraw Drawing]`;
162
+ case "embed":
163
+ const embedUrl = sanitizeUrl(node.attrs?.src || "");
164
+ return `🔗 [Embedded Content](${embedUrl})`;
165
+ case "subpages":
166
+ return "{{SUBPAGES}}";
167
+ default:
168
+ process.stderr.write(`Warning: unknown node type '${type}', rendering as plain text.\n`);
169
+ return nodeContent.map(processNode).join("");
170
+ }
171
+ };
172
+ const processListItem = (item, prefix) => {
173
+ const itemContent = item.content || [];
174
+ const lines = itemContent.map(processNode);
175
+ return lines
176
+ .map((line, i) => i === 0 ? `${prefix} ${line}` : ` ${line}`)
177
+ .join("\n");
178
+ };
179
+ const processTaskItem = (item) => {
180
+ const checked = item.attrs?.checked || false;
181
+ const checkbox = checked ? "[x]" : "[ ]";
182
+ const itemContent = item.content || [];
183
+ const text = itemContent.map(processNode).join("");
184
+ return `- ${checkbox} ${text}`;
185
+ };
186
+ return processNode(content).trim();
187
+ }