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.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/build/__tests__/cli-utils.test.js +287 -0
- package/build/__tests__/client-pagination.test.js +103 -0
- package/build/__tests__/discovery.test.js +40 -0
- package/build/__tests__/envelope.test.js +91 -0
- package/build/__tests__/filters.test.js +235 -0
- package/build/__tests__/integration/comment.test.js +48 -0
- package/build/__tests__/integration/discovery.test.js +24 -0
- package/build/__tests__/integration/file.test.js +33 -0
- package/build/__tests__/integration/group.test.js +48 -0
- package/build/__tests__/integration/helpers/global-setup.js +80 -0
- package/build/__tests__/integration/helpers/run-cli.js +163 -0
- package/build/__tests__/integration/invite.test.js +34 -0
- package/build/__tests__/integration/page.test.js +69 -0
- package/build/__tests__/integration/search.test.js +45 -0
- package/build/__tests__/integration/share.test.js +49 -0
- package/build/__tests__/integration/space.test.js +56 -0
- package/build/__tests__/integration/user.test.js +15 -0
- package/build/__tests__/integration/workspace.test.js +42 -0
- package/build/__tests__/markdown-converter.test.js +445 -0
- package/build/__tests__/mcp-tooling.test.js +58 -0
- package/build/__tests__/page-mentions.test.js +65 -0
- package/build/__tests__/tiptap-extensions.test.js +135 -0
- package/build/client.js +715 -0
- package/build/commands/comment.js +54 -0
- package/build/commands/discovery.js +21 -0
- package/build/commands/file.js +36 -0
- package/build/commands/group.js +91 -0
- package/build/commands/invite.js +67 -0
- package/build/commands/page.js +227 -0
- package/build/commands/search.js +33 -0
- package/build/commands/share.js +65 -0
- package/build/commands/space.js +154 -0
- package/build/commands/user.js +38 -0
- package/build/commands/workspace.js +77 -0
- package/build/index.js +19 -0
- package/build/lib/auth-utils.js +53 -0
- package/build/lib/cli-utils.js +293 -0
- package/build/lib/collaboration.js +126 -0
- package/build/lib/filters.js +137 -0
- package/build/lib/markdown-converter.js +187 -0
- package/build/lib/mcp-tooling.js +295 -0
- package/build/lib/page-mentions.js +162 -0
- package/build/lib/tiptap-extensions.js +86 -0
- package/build/mcp.js +186 -0
- package/build/program.js +60 -0
- package/package.json +64 -0
package/build/client.js
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { createReadStream, accessSync, constants as fsConstants } from "fs";
|
|
2
|
+
import FormData from "form-data";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { filterWorkspace, filterSpace, filterGroup, filterPage, filterSearchResult, filterHistoryEntry, filterHistoryDetail, filterMember, filterInvite, filterUser, filterComment, filterShare, } from "./lib/filters.js";
|
|
5
|
+
import { convertProseMirrorToMarkdown } from "./lib/markdown-converter.js";
|
|
6
|
+
import { updatePageContentRealtime } from "./lib/collaboration.js";
|
|
7
|
+
import { getCollabToken, performLogin } from "./lib/auth-utils.js";
|
|
8
|
+
import { markdownToProseMirrorJson, } from "./lib/page-mentions.js";
|
|
9
|
+
function ensureFileReadable(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
accessSync(filePath, fsConstants.R_OK);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
const code = err.code ?? "UNKNOWN";
|
|
15
|
+
throw new Error(`File not found or not readable: ${filePath} (${code})`, { cause: err });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function normalizeMentionLabel(value) {
|
|
19
|
+
return value.trim().replace(/\s+/g, " ").toLocaleLowerCase();
|
|
20
|
+
}
|
|
21
|
+
export class DocmostClient {
|
|
22
|
+
client;
|
|
23
|
+
baseURL;
|
|
24
|
+
auth;
|
|
25
|
+
token;
|
|
26
|
+
constructor(baseURL, auth = {}) {
|
|
27
|
+
this.baseURL = baseURL.replace(/\/+$/, "");
|
|
28
|
+
this.auth = auth;
|
|
29
|
+
this.token = auth.token ?? null;
|
|
30
|
+
this.client = axios.create({
|
|
31
|
+
baseURL: this.baseURL,
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (this.token) {
|
|
37
|
+
this.client.defaults.headers.common["Authorization"] =
|
|
38
|
+
`Bearer ${this.token}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async login() {
|
|
42
|
+
if (this.token) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!this.auth.email || !this.auth.password) {
|
|
46
|
+
throw new Error("Missing credentials. Provide token or email/password.");
|
|
47
|
+
}
|
|
48
|
+
this.token = await performLogin(this.baseURL, this.auth.email, this.auth.password);
|
|
49
|
+
this.client.defaults.headers.common["Authorization"] =
|
|
50
|
+
`Bearer ${this.token}`;
|
|
51
|
+
}
|
|
52
|
+
async ensureAuthenticated() {
|
|
53
|
+
if (!this.token) {
|
|
54
|
+
await this.login();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async paginateAll(endpoint, basePayload = {}, limit = 100, maxItems = Infinity) {
|
|
58
|
+
await this.ensureAuthenticated();
|
|
59
|
+
const clampedLimit = Math.max(1, Math.min(100, limit));
|
|
60
|
+
let page = 1;
|
|
61
|
+
let cursor = null;
|
|
62
|
+
let allItems = [];
|
|
63
|
+
let hasNextPage = true;
|
|
64
|
+
const seenRequests = new Set();
|
|
65
|
+
while (hasNextPage && allItems.length < maxItems) {
|
|
66
|
+
const usingCursor = cursor !== null;
|
|
67
|
+
const requestKey = usingCursor ? `cursor:${cursor}` : `page:${page}`;
|
|
68
|
+
if (seenRequests.has(requestKey)) {
|
|
69
|
+
throw new Error(`Pagination loop detected for ${endpoint}: repeated ${requestKey}`);
|
|
70
|
+
}
|
|
71
|
+
seenRequests.add(requestKey);
|
|
72
|
+
const requestPayload = {
|
|
73
|
+
...basePayload,
|
|
74
|
+
limit: clampedLimit,
|
|
75
|
+
...(usingCursor ? { cursor } : { page }),
|
|
76
|
+
};
|
|
77
|
+
const response = await this.client.post(endpoint, requestPayload);
|
|
78
|
+
const data = response.data;
|
|
79
|
+
const inner = data.data ?? data;
|
|
80
|
+
const items = inner.items;
|
|
81
|
+
if (!Array.isArray(items)) {
|
|
82
|
+
throw new Error(`Unexpected API response from ${endpoint}: missing items array`);
|
|
83
|
+
}
|
|
84
|
+
const meta = inner.meta;
|
|
85
|
+
if (!meta && items.length === clampedLimit) {
|
|
86
|
+
process.stderr.write(`Warning: API response from ${endpoint} missing pagination meta; results may be incomplete.\n`);
|
|
87
|
+
}
|
|
88
|
+
allItems = allItems.concat(items);
|
|
89
|
+
const nextCursor = typeof meta?.nextCursor === "string" && meta.nextCursor.length > 0
|
|
90
|
+
? meta.nextCursor
|
|
91
|
+
: null;
|
|
92
|
+
const usesCursorPagination = usingCursor || nextCursor !== null || typeof meta?.prevCursor === "string";
|
|
93
|
+
if (usesCursorPagination) {
|
|
94
|
+
hasNextPage = Boolean(meta?.hasNextPage && nextCursor);
|
|
95
|
+
cursor = hasNextPage ? nextCursor : null;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
hasNextPage = meta?.hasNextPage ?? false;
|
|
99
|
+
page++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const finalItems = maxItems < Infinity ? allItems.slice(0, maxItems) : allItems;
|
|
103
|
+
const truncated = finalItems.length < allItems.length;
|
|
104
|
+
return { items: finalItems, hasMore: truncated || hasNextPage };
|
|
105
|
+
}
|
|
106
|
+
async getWorkspace() {
|
|
107
|
+
await this.ensureAuthenticated();
|
|
108
|
+
const response = await this.client.post("/workspace/info", {});
|
|
109
|
+
return {
|
|
110
|
+
data: filterWorkspace(response.data.data),
|
|
111
|
+
success: response.data.success,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async getSpaces() {
|
|
115
|
+
const result = await this.paginateAll("/spaces", {});
|
|
116
|
+
return { items: result.items.map((space) => filterSpace(space)), hasMore: result.hasMore };
|
|
117
|
+
}
|
|
118
|
+
async getGroups() {
|
|
119
|
+
const result = await this.paginateAll("/groups", {});
|
|
120
|
+
return { items: result.items.map((group) => filterGroup(group)), hasMore: result.hasMore };
|
|
121
|
+
}
|
|
122
|
+
async listPages(spaceId) {
|
|
123
|
+
const payload = spaceId ? { spaceId } : {};
|
|
124
|
+
const result = await this.paginateAll("/pages/recent", payload);
|
|
125
|
+
return { items: result.items.map((page) => filterPage(page)), hasMore: result.hasMore };
|
|
126
|
+
}
|
|
127
|
+
async listSidebarPages(spaceId, pageId) {
|
|
128
|
+
await this.ensureAuthenticated();
|
|
129
|
+
const response = await this.client.post("/pages/sidebar-pages", {
|
|
130
|
+
spaceId,
|
|
131
|
+
pageId,
|
|
132
|
+
page: 1,
|
|
133
|
+
});
|
|
134
|
+
const items = response.data?.data?.items;
|
|
135
|
+
if (items !== undefined && !Array.isArray(items)) {
|
|
136
|
+
throw new Error("Unexpected API response from /pages/sidebar-pages: items is not an array");
|
|
137
|
+
}
|
|
138
|
+
return items ?? [];
|
|
139
|
+
}
|
|
140
|
+
async getPage(pageId) {
|
|
141
|
+
await this.ensureAuthenticated();
|
|
142
|
+
const response = await this.client.post("/pages/info", { pageId });
|
|
143
|
+
const resultData = response.data.data;
|
|
144
|
+
let content = resultData.content
|
|
145
|
+
? convertProseMirrorToMarkdown(resultData.content)
|
|
146
|
+
: "";
|
|
147
|
+
let subpages = [];
|
|
148
|
+
try {
|
|
149
|
+
subpages = await this.listSidebarPages(resultData.spaceId, pageId);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (axios.isAxiosError(error) && (error.response?.status === 401 || error.response?.status === 403)) {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
156
|
+
process.stderr.write(`Warning: failed to fetch subpages: ${msg}\n`);
|
|
157
|
+
}
|
|
158
|
+
if (content && content.includes("{{SUBPAGES}}")) {
|
|
159
|
+
if (subpages.length > 0) {
|
|
160
|
+
const list = subpages
|
|
161
|
+
.map((p) => `- [${p.title}](page:${p.id})`)
|
|
162
|
+
.join("\n");
|
|
163
|
+
content = content.replaceAll("{{SUBPAGES}}", `### Subpages\n${list}`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
content = content.replaceAll("{{SUBPAGES}}", "");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
data: filterPage(resultData, content, subpages),
|
|
171
|
+
success: response.data.success,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async createPage(spaceId, title, icon, parentPageId) {
|
|
175
|
+
await this.ensureAuthenticated();
|
|
176
|
+
const response = await this.client.post("/pages/create", {
|
|
177
|
+
spaceId,
|
|
178
|
+
...(title !== undefined && { title }),
|
|
179
|
+
...(icon !== undefined && { icon }),
|
|
180
|
+
...(parentPageId !== undefined && { parentPageId }),
|
|
181
|
+
});
|
|
182
|
+
return response.data.data ?? response.data;
|
|
183
|
+
}
|
|
184
|
+
async getPageTree(spaceId, pageId) {
|
|
185
|
+
await this.ensureAuthenticated();
|
|
186
|
+
if (!spaceId && !pageId)
|
|
187
|
+
throw new Error("At least one of spaceId or pageId is required");
|
|
188
|
+
const payload = {};
|
|
189
|
+
if (spaceId)
|
|
190
|
+
payload.spaceId = spaceId;
|
|
191
|
+
if (pageId)
|
|
192
|
+
payload.pageId = pageId;
|
|
193
|
+
const response = await this.client.post("/pages/sidebar-pages", { ...payload, page: 1 });
|
|
194
|
+
const items = response.data?.data?.items;
|
|
195
|
+
if (!Array.isArray(items)) {
|
|
196
|
+
throw new Error("Unexpected page tree response structure from API.");
|
|
197
|
+
}
|
|
198
|
+
return items;
|
|
199
|
+
}
|
|
200
|
+
async movePageToSpace(pageId, spaceId) {
|
|
201
|
+
await this.ensureAuthenticated();
|
|
202
|
+
const response = await this.client.post("/pages/move-to-space", { pageId, spaceId });
|
|
203
|
+
return response.data;
|
|
204
|
+
}
|
|
205
|
+
async exportPage(pageId, format, includeChildren, includeAttachments) {
|
|
206
|
+
await this.ensureAuthenticated();
|
|
207
|
+
const response = await this.client.post("/pages/export", {
|
|
208
|
+
pageId, format,
|
|
209
|
+
...(includeChildren !== undefined && { includeChildren }),
|
|
210
|
+
...(includeAttachments !== undefined && { includeAttachments }),
|
|
211
|
+
}, { responseType: "arraybuffer" });
|
|
212
|
+
return response.data;
|
|
213
|
+
}
|
|
214
|
+
async importPage(filePath, spaceId) {
|
|
215
|
+
await this.ensureAuthenticated();
|
|
216
|
+
ensureFileReadable(filePath);
|
|
217
|
+
const form = new FormData();
|
|
218
|
+
form.append("spaceId", spaceId);
|
|
219
|
+
form.append("file", createReadStream(filePath));
|
|
220
|
+
const response = await axios.post(`${this.baseURL}/pages/import`, form, {
|
|
221
|
+
headers: { ...form.getHeaders(), Authorization: `Bearer ${this.token}` },
|
|
222
|
+
});
|
|
223
|
+
return response.data;
|
|
224
|
+
}
|
|
225
|
+
async importZip(filePath, spaceId, source) {
|
|
226
|
+
await this.ensureAuthenticated();
|
|
227
|
+
ensureFileReadable(filePath);
|
|
228
|
+
const form = new FormData();
|
|
229
|
+
form.append("spaceId", spaceId);
|
|
230
|
+
form.append("source", source);
|
|
231
|
+
form.append("file", createReadStream(filePath));
|
|
232
|
+
const response = await axios.post(`${this.baseURL}/pages/import-zip`, form, {
|
|
233
|
+
headers: { ...form.getHeaders(), Authorization: `Bearer ${this.token}` },
|
|
234
|
+
});
|
|
235
|
+
return response.data;
|
|
236
|
+
}
|
|
237
|
+
async updatePage(pageId, content, title, icon) {
|
|
238
|
+
await this.ensureAuthenticated();
|
|
239
|
+
const metadata = { pageId };
|
|
240
|
+
if (title !== undefined)
|
|
241
|
+
metadata.title = title;
|
|
242
|
+
if (icon !== undefined)
|
|
243
|
+
metadata.icon = icon;
|
|
244
|
+
if (Object.keys(metadata).length > 1) {
|
|
245
|
+
await this.client.post("/pages/update", metadata);
|
|
246
|
+
}
|
|
247
|
+
if (content !== undefined) {
|
|
248
|
+
if (!this.token) {
|
|
249
|
+
throw new Error("Authentication token is required for content updates");
|
|
250
|
+
}
|
|
251
|
+
let collabToken = "";
|
|
252
|
+
try {
|
|
253
|
+
collabToken = await getCollabToken(this.baseURL, this.token);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (axios.isAxiosError(error))
|
|
257
|
+
throw error;
|
|
258
|
+
const cause = error instanceof Error ? error : undefined;
|
|
259
|
+
throw new Error(`Failed to get collaboration token: ${cause?.message ?? String(error)}`, { cause });
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const prosemirrorJson = await this.buildPageContentJson(pageId, content);
|
|
263
|
+
await updatePageContentRealtime(pageId, prosemirrorJson, collabToken, this.baseURL);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (axios.isAxiosError(error))
|
|
267
|
+
throw error;
|
|
268
|
+
const cause = error instanceof Error ? error : undefined;
|
|
269
|
+
throw new Error(`Failed to update page content: ${cause?.message ?? String(error)}`, { cause });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
modified: true,
|
|
275
|
+
message: "Page updated successfully.",
|
|
276
|
+
pageId,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async buildPageContentJson(pageId, content) {
|
|
280
|
+
const pageResponse = await this.client.post("/pages/info", { pageId });
|
|
281
|
+
const page = pageResponse.data?.data ?? pageResponse.data;
|
|
282
|
+
const spaceId = typeof page?.spaceId === "string" ? page.spaceId : undefined;
|
|
283
|
+
const currentUser = await this.getCurrentUser();
|
|
284
|
+
const mentionCache = new Map();
|
|
285
|
+
try {
|
|
286
|
+
return await markdownToProseMirrorJson(content, {
|
|
287
|
+
creatorId: currentUser.id,
|
|
288
|
+
resolvePageMention: async (label) => {
|
|
289
|
+
const cacheKey = normalizeMentionLabel(label);
|
|
290
|
+
if (mentionCache.has(cacheKey)) {
|
|
291
|
+
return mentionCache.get(cacheKey) ?? null;
|
|
292
|
+
}
|
|
293
|
+
const resolved = await this.resolvePageMention(label, spaceId);
|
|
294
|
+
mentionCache.set(cacheKey, resolved);
|
|
295
|
+
return resolved;
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
const cause = error instanceof Error ? error : undefined;
|
|
301
|
+
throw new Error(`Failed to convert markdown to ProseMirror JSON: ${cause?.message ?? String(error)}`, { cause });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async resolvePageMention(label, preferredSpaceId) {
|
|
305
|
+
const normalizedLabel = normalizeMentionLabel(label);
|
|
306
|
+
const selectSingleMatch = (pages, scope) => {
|
|
307
|
+
const matches = pages.filter((page) => normalizeMentionLabel(page.title || "") === normalizedLabel);
|
|
308
|
+
if (matches.length === 1) {
|
|
309
|
+
const match = matches[0];
|
|
310
|
+
return { id: match.id, title: match.title, slugId: match.slugId };
|
|
311
|
+
}
|
|
312
|
+
if (matches.length > 1) {
|
|
313
|
+
process.stderr.write(`Warning: mention @${label} is ambiguous in ${scope}; keeping plain text.\n`);
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
};
|
|
317
|
+
if (preferredSpaceId) {
|
|
318
|
+
const scoped = await this.searchSuggest(label, preferredSpaceId, {
|
|
319
|
+
includePages: true,
|
|
320
|
+
includeGroups: false,
|
|
321
|
+
includeUsers: false,
|
|
322
|
+
limit: 20,
|
|
323
|
+
});
|
|
324
|
+
const scopedPages = Array.isArray(scoped?.pages)
|
|
325
|
+
? scoped.pages
|
|
326
|
+
: [];
|
|
327
|
+
const scopedMatch = selectSingleMatch(scopedPages, `space ${preferredSpaceId}`);
|
|
328
|
+
if (scopedMatch) {
|
|
329
|
+
return scopedMatch;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const workspaceWide = await this.searchSuggest(label, undefined, {
|
|
333
|
+
includePages: true,
|
|
334
|
+
includeGroups: false,
|
|
335
|
+
includeUsers: false,
|
|
336
|
+
limit: 20,
|
|
337
|
+
});
|
|
338
|
+
const workspacePages = Array.isArray(workspaceWide?.pages)
|
|
339
|
+
? workspaceWide.pages
|
|
340
|
+
: [];
|
|
341
|
+
return selectSingleMatch(workspacePages, "workspace");
|
|
342
|
+
}
|
|
343
|
+
async search(query, spaceId, creatorId) {
|
|
344
|
+
await this.ensureAuthenticated();
|
|
345
|
+
const response = await this.client.post("/search", {
|
|
346
|
+
query,
|
|
347
|
+
...(spaceId !== undefined && { spaceId }),
|
|
348
|
+
...(creatorId !== undefined && { creatorId }),
|
|
349
|
+
});
|
|
350
|
+
const items = response.data?.data?.items;
|
|
351
|
+
if (items !== undefined && !Array.isArray(items)) {
|
|
352
|
+
throw new Error("Unexpected API response from /search: items is not an array");
|
|
353
|
+
}
|
|
354
|
+
const filteredItems = (items ?? []).map((item) => filterSearchResult(item));
|
|
355
|
+
return {
|
|
356
|
+
items: filteredItems,
|
|
357
|
+
hasMore: false,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async movePage(pageId, parentPageId, position) {
|
|
361
|
+
await this.ensureAuthenticated();
|
|
362
|
+
const validPosition = position || "a00000";
|
|
363
|
+
const response = await this.client.post("/pages/move", {
|
|
364
|
+
pageId,
|
|
365
|
+
parentPageId,
|
|
366
|
+
position: validPosition,
|
|
367
|
+
});
|
|
368
|
+
return response.data;
|
|
369
|
+
}
|
|
370
|
+
async deletePage(pageId, permanentlyDelete = false) {
|
|
371
|
+
await this.ensureAuthenticated();
|
|
372
|
+
const response = await this.client.post("/pages/delete", {
|
|
373
|
+
pageId,
|
|
374
|
+
permanentlyDelete,
|
|
375
|
+
});
|
|
376
|
+
return response.data;
|
|
377
|
+
}
|
|
378
|
+
async deletePages(pageIds) {
|
|
379
|
+
await this.ensureAuthenticated();
|
|
380
|
+
const results = [];
|
|
381
|
+
for (const id of pageIds) {
|
|
382
|
+
try {
|
|
383
|
+
await this.client.post("/pages/delete", { pageId: id });
|
|
384
|
+
results.push({ id, success: true });
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (axios.isAxiosError(error) && (error.response?.status === 401 || error.response?.status === 403)) {
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
391
|
+
results.push({ id, success: false, error: msg });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return results;
|
|
395
|
+
}
|
|
396
|
+
async getPageHistory(pageId, limit, maxItems) {
|
|
397
|
+
const result = await this.paginateAll("/pages/history", { pageId }, limit, maxItems);
|
|
398
|
+
return { items: result.items.map((entry) => filterHistoryEntry(entry)), hasMore: result.hasMore };
|
|
399
|
+
}
|
|
400
|
+
async getPageHistoryDetail(historyId) {
|
|
401
|
+
await this.ensureAuthenticated();
|
|
402
|
+
const response = await this.client.post("/pages/history/info", {
|
|
403
|
+
historyId,
|
|
404
|
+
});
|
|
405
|
+
const entry = response.data.data ?? response.data;
|
|
406
|
+
const content = entry.content
|
|
407
|
+
? convertProseMirrorToMarkdown(entry.content)
|
|
408
|
+
: "";
|
|
409
|
+
return filterHistoryDetail(entry, content);
|
|
410
|
+
}
|
|
411
|
+
async restorePage(pageId) {
|
|
412
|
+
await this.ensureAuthenticated();
|
|
413
|
+
const response = await this.client.post("/pages/restore", { pageId });
|
|
414
|
+
return response.data;
|
|
415
|
+
}
|
|
416
|
+
async getTrash(spaceId) {
|
|
417
|
+
const result = await this.paginateAll("/pages/trash", { spaceId });
|
|
418
|
+
return { items: result.items.map((page) => filterPage(page)), hasMore: result.hasMore };
|
|
419
|
+
}
|
|
420
|
+
async duplicatePage(pageId, spaceId) {
|
|
421
|
+
await this.ensureAuthenticated();
|
|
422
|
+
const payload = { pageId };
|
|
423
|
+
if (spaceId) {
|
|
424
|
+
payload.spaceId = spaceId;
|
|
425
|
+
}
|
|
426
|
+
const response = await this.client.post("/pages/duplicate", payload);
|
|
427
|
+
const newPage = response.data.data ?? response.data;
|
|
428
|
+
return filterPage(newPage);
|
|
429
|
+
}
|
|
430
|
+
async getSpaceInfo(spaceId) {
|
|
431
|
+
await this.ensureAuthenticated();
|
|
432
|
+
const response = await this.client.post("/spaces/info", { spaceId });
|
|
433
|
+
return filterSpace(response.data.data ?? response.data);
|
|
434
|
+
}
|
|
435
|
+
async createSpace(name, slug, description) {
|
|
436
|
+
await this.ensureAuthenticated();
|
|
437
|
+
const response = await this.client.post("/spaces/create", {
|
|
438
|
+
name, ...(slug !== undefined && { slug }), ...(description !== undefined && { description }),
|
|
439
|
+
});
|
|
440
|
+
return filterSpace(response.data.data ?? response.data);
|
|
441
|
+
}
|
|
442
|
+
async updateSpace(spaceId, params) {
|
|
443
|
+
await this.ensureAuthenticated();
|
|
444
|
+
const response = await this.client.post("/spaces/update", { spaceId, ...params });
|
|
445
|
+
return response.data;
|
|
446
|
+
}
|
|
447
|
+
async deleteSpace(spaceId) {
|
|
448
|
+
await this.ensureAuthenticated();
|
|
449
|
+
const response = await this.client.post("/spaces/delete", { spaceId });
|
|
450
|
+
return response.data;
|
|
451
|
+
}
|
|
452
|
+
async exportSpace(spaceId, exportFormat, includeAttachments) {
|
|
453
|
+
await this.ensureAuthenticated();
|
|
454
|
+
const response = await this.client.post("/spaces/export", {
|
|
455
|
+
spaceId,
|
|
456
|
+
...(exportFormat !== undefined && { format: exportFormat }),
|
|
457
|
+
...(includeAttachments !== undefined && { includeAttachments }),
|
|
458
|
+
}, { responseType: "arraybuffer" });
|
|
459
|
+
return response.data;
|
|
460
|
+
}
|
|
461
|
+
async getSpaceMembers(spaceId) {
|
|
462
|
+
const result = await this.paginateAll("/spaces/members", { spaceId });
|
|
463
|
+
return { items: result.items, hasMore: result.hasMore };
|
|
464
|
+
}
|
|
465
|
+
async addSpaceMembers(spaceId, role, userIds, groupIds) {
|
|
466
|
+
await this.ensureAuthenticated();
|
|
467
|
+
const response = await this.client.post("/spaces/members/add", {
|
|
468
|
+
spaceId, role, userIds: userIds ?? [], groupIds: groupIds ?? [],
|
|
469
|
+
});
|
|
470
|
+
return response.data;
|
|
471
|
+
}
|
|
472
|
+
async removeSpaceMember(spaceId, userId, groupId) {
|
|
473
|
+
await this.ensureAuthenticated();
|
|
474
|
+
const payload = { spaceId };
|
|
475
|
+
if (userId)
|
|
476
|
+
payload.userId = userId;
|
|
477
|
+
if (groupId)
|
|
478
|
+
payload.groupId = groupId;
|
|
479
|
+
const response = await this.client.post("/spaces/members/remove", payload);
|
|
480
|
+
return response.data;
|
|
481
|
+
}
|
|
482
|
+
async changeSpaceMemberRole(spaceId, role, userId, groupId) {
|
|
483
|
+
await this.ensureAuthenticated();
|
|
484
|
+
const payload = { spaceId, role };
|
|
485
|
+
if (userId)
|
|
486
|
+
payload.userId = userId;
|
|
487
|
+
if (groupId)
|
|
488
|
+
payload.groupId = groupId;
|
|
489
|
+
const response = await this.client.post("/spaces/members/change-role", payload);
|
|
490
|
+
return response.data;
|
|
491
|
+
}
|
|
492
|
+
async getWorkspacePublic() {
|
|
493
|
+
const response = await this.client.post("/workspace/public", {});
|
|
494
|
+
return response.data;
|
|
495
|
+
}
|
|
496
|
+
async updateWorkspace(params) {
|
|
497
|
+
await this.ensureAuthenticated();
|
|
498
|
+
const response = await this.client.post("/workspace/update", params);
|
|
499
|
+
return response.data;
|
|
500
|
+
}
|
|
501
|
+
async getMembers() {
|
|
502
|
+
const result = await this.paginateAll("/workspace/members", {});
|
|
503
|
+
return { items: result.items.map((m) => filterMember(m)), hasMore: result.hasMore };
|
|
504
|
+
}
|
|
505
|
+
async removeMember(userId) {
|
|
506
|
+
await this.ensureAuthenticated();
|
|
507
|
+
const response = await this.client.post("/workspace/members/delete", { userId });
|
|
508
|
+
return response.data;
|
|
509
|
+
}
|
|
510
|
+
async changeMemberRole(userId, role) {
|
|
511
|
+
await this.ensureAuthenticated();
|
|
512
|
+
const response = await this.client.post("/workspace/members/change-role", { userId, role });
|
|
513
|
+
return response.data;
|
|
514
|
+
}
|
|
515
|
+
// Invite methods
|
|
516
|
+
async getInvites() {
|
|
517
|
+
const result = await this.paginateAll("/workspace/invites", {});
|
|
518
|
+
return { items: result.items.map((i) => filterInvite(i)), hasMore: result.hasMore };
|
|
519
|
+
}
|
|
520
|
+
async getInviteInfo(invitationId) {
|
|
521
|
+
await this.ensureAuthenticated();
|
|
522
|
+
const response = await this.client.post("/workspace/invites/info", { invitationId });
|
|
523
|
+
return filterInvite(response.data.data ?? response.data);
|
|
524
|
+
}
|
|
525
|
+
async createInvite(emails, role, groupIds) {
|
|
526
|
+
await this.ensureAuthenticated();
|
|
527
|
+
const response = await this.client.post("/workspace/invites/create", {
|
|
528
|
+
emails,
|
|
529
|
+
role,
|
|
530
|
+
groupIds: groupIds ?? [],
|
|
531
|
+
});
|
|
532
|
+
return response.data;
|
|
533
|
+
}
|
|
534
|
+
async revokeInvite(invitationId) {
|
|
535
|
+
await this.ensureAuthenticated();
|
|
536
|
+
const response = await this.client.post("/workspace/invites/revoke", { invitationId });
|
|
537
|
+
return response.data;
|
|
538
|
+
}
|
|
539
|
+
async resendInvite(invitationId) {
|
|
540
|
+
await this.ensureAuthenticated();
|
|
541
|
+
const response = await this.client.post("/workspace/invites/resend", { invitationId });
|
|
542
|
+
return response.data;
|
|
543
|
+
}
|
|
544
|
+
async getInviteLink(invitationId) {
|
|
545
|
+
await this.ensureAuthenticated();
|
|
546
|
+
const response = await this.client.post("/workspace/invites/link", { invitationId });
|
|
547
|
+
return response.data.data ?? response.data;
|
|
548
|
+
}
|
|
549
|
+
// User methods
|
|
550
|
+
async getCurrentUser() {
|
|
551
|
+
await this.ensureAuthenticated();
|
|
552
|
+
const response = await this.client.post("/users/me", {});
|
|
553
|
+
return filterUser(response.data.data ?? response.data);
|
|
554
|
+
}
|
|
555
|
+
async updateUser(params) {
|
|
556
|
+
await this.ensureAuthenticated();
|
|
557
|
+
const response = await this.client.post("/users/update", params);
|
|
558
|
+
return response.data;
|
|
559
|
+
}
|
|
560
|
+
async getGroupInfo(groupId) {
|
|
561
|
+
await this.ensureAuthenticated();
|
|
562
|
+
const response = await this.client.post("/groups/info", { groupId });
|
|
563
|
+
return filterGroup(response.data.data ?? response.data);
|
|
564
|
+
}
|
|
565
|
+
async createGroup(name, description, userIds) {
|
|
566
|
+
await this.ensureAuthenticated();
|
|
567
|
+
const response = await this.client.post("/groups/create", {
|
|
568
|
+
name, ...(description !== undefined && { description }), ...(userIds !== undefined && { userIds }),
|
|
569
|
+
});
|
|
570
|
+
return filterGroup(response.data.data ?? response.data);
|
|
571
|
+
}
|
|
572
|
+
async updateGroup(groupId, params) {
|
|
573
|
+
await this.ensureAuthenticated();
|
|
574
|
+
const response = await this.client.post("/groups/update", { groupId, ...params });
|
|
575
|
+
return response.data;
|
|
576
|
+
}
|
|
577
|
+
async deleteGroup(groupId) {
|
|
578
|
+
await this.ensureAuthenticated();
|
|
579
|
+
const response = await this.client.post("/groups/delete", { groupId });
|
|
580
|
+
return response.data;
|
|
581
|
+
}
|
|
582
|
+
async getGroupMembers(groupId) {
|
|
583
|
+
const result = await this.paginateAll("/groups/members", { groupId });
|
|
584
|
+
return { items: result.items, hasMore: result.hasMore };
|
|
585
|
+
}
|
|
586
|
+
async addGroupMembers(groupId, userIds) {
|
|
587
|
+
await this.ensureAuthenticated();
|
|
588
|
+
const response = await this.client.post("/groups/members/add", { groupId, userIds });
|
|
589
|
+
return response.data;
|
|
590
|
+
}
|
|
591
|
+
async removeGroupMember(groupId, userId) {
|
|
592
|
+
await this.ensureAuthenticated();
|
|
593
|
+
const response = await this.client.post("/groups/members/remove", { groupId, userId });
|
|
594
|
+
return response.data;
|
|
595
|
+
}
|
|
596
|
+
async getPageBreadcrumbs(pageId) {
|
|
597
|
+
await this.ensureAuthenticated();
|
|
598
|
+
const response = await this.client.post("/pages/breadcrumbs", { pageId });
|
|
599
|
+
const items = response.data.data ?? response.data;
|
|
600
|
+
if (!Array.isArray(items)) {
|
|
601
|
+
process.stderr.write(`Warning: getPageBreadcrumbs returned non-array response\n`);
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
return items.map((breadcrumb) => ({
|
|
605
|
+
id: breadcrumb.id,
|
|
606
|
+
title: breadcrumb.title,
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
// Comment methods
|
|
610
|
+
async getComments(pageId) {
|
|
611
|
+
const result = await this.paginateAll("/comments", { pageId });
|
|
612
|
+
return { items: result.items.map((c) => filterComment(c)), hasMore: result.hasMore };
|
|
613
|
+
}
|
|
614
|
+
async getCommentInfo(commentId) {
|
|
615
|
+
await this.ensureAuthenticated();
|
|
616
|
+
const response = await this.client.post("/comments/info", { commentId });
|
|
617
|
+
return filterComment(response.data.data ?? response.data);
|
|
618
|
+
}
|
|
619
|
+
async createComment(pageId, content, selection, parentCommentId) {
|
|
620
|
+
await this.ensureAuthenticated();
|
|
621
|
+
const prosemirrorJson = await markdownToProseMirrorJson(content);
|
|
622
|
+
const response = await this.client.post("/comments/create", {
|
|
623
|
+
pageId,
|
|
624
|
+
content: JSON.stringify(prosemirrorJson),
|
|
625
|
+
...(selection !== undefined && { selection }),
|
|
626
|
+
...(parentCommentId !== undefined && { parentCommentId }),
|
|
627
|
+
});
|
|
628
|
+
return response.data;
|
|
629
|
+
}
|
|
630
|
+
async updateComment(commentId, content) {
|
|
631
|
+
await this.ensureAuthenticated();
|
|
632
|
+
const prosemirrorJson = await markdownToProseMirrorJson(content);
|
|
633
|
+
const response = await this.client.post("/comments/update", {
|
|
634
|
+
commentId,
|
|
635
|
+
content: JSON.stringify(prosemirrorJson),
|
|
636
|
+
});
|
|
637
|
+
return response.data;
|
|
638
|
+
}
|
|
639
|
+
async deleteComment(commentId) {
|
|
640
|
+
await this.ensureAuthenticated();
|
|
641
|
+
const response = await this.client.post("/comments/delete", { commentId });
|
|
642
|
+
return response.data;
|
|
643
|
+
}
|
|
644
|
+
// Share methods
|
|
645
|
+
async getShares() {
|
|
646
|
+
const result = await this.paginateAll("/shares", {});
|
|
647
|
+
return { items: result.items.map((s) => filterShare(s)), hasMore: result.hasMore };
|
|
648
|
+
}
|
|
649
|
+
async getShareInfo(shareId) {
|
|
650
|
+
await this.ensureAuthenticated();
|
|
651
|
+
const response = await this.client.post("/shares/info", { shareId });
|
|
652
|
+
return filterShare(response.data.data ?? response.data);
|
|
653
|
+
}
|
|
654
|
+
async getShareForPage(pageId) {
|
|
655
|
+
await this.ensureAuthenticated();
|
|
656
|
+
const response = await this.client.post("/shares/for-page", { pageId });
|
|
657
|
+
return filterShare(response.data.data ?? response.data);
|
|
658
|
+
}
|
|
659
|
+
async createShare(pageId, includeSubPages, searchIndexing) {
|
|
660
|
+
await this.ensureAuthenticated();
|
|
661
|
+
const response = await this.client.post("/shares/create", {
|
|
662
|
+
pageId,
|
|
663
|
+
...(includeSubPages !== undefined && { includeSubPages }),
|
|
664
|
+
...(searchIndexing !== undefined && { searchIndexing }),
|
|
665
|
+
});
|
|
666
|
+
return filterShare(response.data.data ?? response.data);
|
|
667
|
+
}
|
|
668
|
+
async updateShare(shareId, includeSubPages, searchIndexing) {
|
|
669
|
+
await this.ensureAuthenticated();
|
|
670
|
+
const response = await this.client.post("/shares/update", {
|
|
671
|
+
shareId,
|
|
672
|
+
...(includeSubPages !== undefined && { includeSubPages }),
|
|
673
|
+
...(searchIndexing !== undefined && { searchIndexing }),
|
|
674
|
+
});
|
|
675
|
+
return response.data;
|
|
676
|
+
}
|
|
677
|
+
async deleteShare(shareId) {
|
|
678
|
+
await this.ensureAuthenticated();
|
|
679
|
+
const response = await this.client.post("/shares/delete", { shareId });
|
|
680
|
+
return response.data;
|
|
681
|
+
}
|
|
682
|
+
// File methods
|
|
683
|
+
async uploadFile(filePath, pageId, attachmentId) {
|
|
684
|
+
await this.ensureAuthenticated();
|
|
685
|
+
ensureFileReadable(filePath);
|
|
686
|
+
const form = new FormData();
|
|
687
|
+
form.append("file", createReadStream(filePath));
|
|
688
|
+
form.append("pageId", pageId);
|
|
689
|
+
if (attachmentId)
|
|
690
|
+
form.append("attachmentId", attachmentId);
|
|
691
|
+
const response = await axios.post(`${this.baseURL}/files/upload`, form, {
|
|
692
|
+
headers: { ...form.getHeaders(), Authorization: `Bearer ${this.token}` },
|
|
693
|
+
});
|
|
694
|
+
return response.data;
|
|
695
|
+
}
|
|
696
|
+
async downloadFile(fileId, fileName) {
|
|
697
|
+
await this.ensureAuthenticated();
|
|
698
|
+
if (!/^[\w-]+$/.test(fileId)) {
|
|
699
|
+
throw new Error(`Invalid file ID: '${fileId}'. Expected alphanumeric/UUID format.`);
|
|
700
|
+
}
|
|
701
|
+
const sanitizedName = fileName.replace(/[/\\]/g, "_").replace(/\.\./g, "_");
|
|
702
|
+
const response = await this.client.get(`/files/${fileId}/${encodeURIComponent(sanitizedName)}`, {
|
|
703
|
+
responseType: "arraybuffer",
|
|
704
|
+
});
|
|
705
|
+
return response.data;
|
|
706
|
+
}
|
|
707
|
+
// Search suggest
|
|
708
|
+
async searchSuggest(query, spaceId, options) {
|
|
709
|
+
await this.ensureAuthenticated();
|
|
710
|
+
const response = await this.client.post("/search/suggest", {
|
|
711
|
+
query, ...(spaceId !== undefined && { spaceId }), ...options,
|
|
712
|
+
});
|
|
713
|
+
return response.data.data ?? response.data;
|
|
714
|
+
}
|
|
715
|
+
}
|