typst-mcp 1.0.2 → 1.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/README.md +49 -87
- package/dist/client/compiler.js +234 -6
- package/dist/client/typst-client.js +306 -52
- package/dist/index.js +97 -5
- package/dist/protocol/edit-diff.js +330 -0
- package/dist/protocol/search-replace.js +97 -0
- package/dist/protocol/yjs-doc.js +31 -38
- package/dist/tools/definitions.js +123 -6
- package/dist/utils/file-mutation-queue.js +30 -0
- package/dist/utils/mime.js +23 -0
- package/dist/utils/truncate.js +114 -0
- package/package.json +3 -1
|
@@ -3,10 +3,14 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { TypstAuthManager } from "./auth.js";
|
|
5
5
|
import { TypstLoadError, formatUnknownError } from "./errors.js";
|
|
6
|
-
import { captureBrowserPreviewScreenshot, captureWorkspacePageScreenshots, compileWorkspaceDiagnostics, compileWorkspacePdf, parseShortDiagnostics, resolveTypstBinary, } from "./compiler.js";
|
|
7
|
-
import { CREATE_PROJECT_MUTATION, DASHBOARD_CONTENT_QUERY, DELETE_FILES_MUTATION, DELETE_PROJECT_MUTATION, PROJECT_FILES_QUERY, UPDATE_FILES_MUTATION, } from "../protocol/graphql.js";
|
|
6
|
+
import { captureBrowserPreviewScreenshot, captureWebDiagnostics, captureWorkspacePageScreenshots, compileWorkspaceDiagnostics, compileWorkspacePdf, hasTypstBinary, parseShortDiagnostics, resolveTypstBinary, } from "./compiler.js";
|
|
7
|
+
import { CREATE_PROJECT_MUTATION, DASHBOARD_CONTENT_QUERY, DELETE_FILES_MUTATION, DELETE_PROJECT_MUTATION, EDIT_PROJECT_MUTATION, PROJECT_FILES_QUERY, UPDATE_FILES_MUTATION, } from "../protocol/graphql.js";
|
|
8
8
|
import { decodeYjsText, editYjsText, replaceYjsText, } from "../protocol/yjs-doc.js";
|
|
9
9
|
import { decodeMessage, encodeMessage, MessageKind, } from "../protocol/ws-codec.js";
|
|
10
|
+
import { truncateHead, formatSize, } from "../utils/truncate.js";
|
|
11
|
+
import { detectSupportedImageMimeType } from "../utils/mime.js";
|
|
12
|
+
import { withFileMutationQueue } from "../utils/file-mutation-queue.js";
|
|
13
|
+
import { matchGlob, searchInContent, replaceInContent, } from "../protocol/search-replace.js";
|
|
10
14
|
export { TypstLoadError, resolveTypstBinary };
|
|
11
15
|
const URL_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
12
16
|
export function generateClientId(size = 8) {
|
|
@@ -215,22 +219,102 @@ export class TypstCloudClient {
|
|
|
215
219
|
const { response } = await this.restCall("GET", `files/${projectId}/${fileId}`);
|
|
216
220
|
return new Uint8Array(await response.arrayBuffer());
|
|
217
221
|
}
|
|
218
|
-
async readFile(projectId, fileIdOrPath) {
|
|
222
|
+
async readFile(projectId, fileIdOrPath, options) {
|
|
219
223
|
const file = await this.resolveFile(projectId, fileIdOrPath);
|
|
220
224
|
if (file.isMultiplayable) {
|
|
221
225
|
const yjsBytes = await this.downloadFileYjs(projectId, file.id);
|
|
222
226
|
const text = decodeYjsText(yjsBytes);
|
|
227
|
+
const allLines = text.split("\n");
|
|
228
|
+
const totalLines = allLines.length;
|
|
229
|
+
if (options?.offset !== undefined && options.offset > totalLines) {
|
|
230
|
+
throw new Error(`Offset ${options.offset} is beyond end of file (${totalLines} lines total)`);
|
|
231
|
+
}
|
|
232
|
+
const startLine = options?.offset ? Math.max(0, options.offset - 1) : 0;
|
|
233
|
+
const startLineDisplay = startLine + 1;
|
|
234
|
+
let selectedContent;
|
|
235
|
+
let userLimitedLines;
|
|
236
|
+
if (options?.limit !== undefined) {
|
|
237
|
+
const endLine = Math.min(startLine + options.limit, totalLines);
|
|
238
|
+
selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
239
|
+
userLimitedLines = endLine - startLine;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
selectedContent = allLines.slice(startLine).join("\n");
|
|
243
|
+
}
|
|
244
|
+
const truncation = truncateHead(selectedContent, {
|
|
245
|
+
maxLines: options?.maxLines,
|
|
246
|
+
maxBytes: options?.maxBytes,
|
|
247
|
+
});
|
|
248
|
+
let outputText;
|
|
249
|
+
let endLineDisplay = startLineDisplay;
|
|
250
|
+
if (truncation.firstLineExceedsLimit) {
|
|
251
|
+
const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine] ?? "", "utf-8"));
|
|
252
|
+
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(truncation.maxBytes)} limit]`;
|
|
253
|
+
}
|
|
254
|
+
else if (truncation.truncated) {
|
|
255
|
+
endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
256
|
+
const nextOffset = endLineDisplay + 1;
|
|
257
|
+
let body = truncation.content;
|
|
258
|
+
if (options?.lineNumbers) {
|
|
259
|
+
const lines = body.split("\n");
|
|
260
|
+
const width = String(endLineDisplay).length;
|
|
261
|
+
body = lines
|
|
262
|
+
.map((line, idx) => `${String(startLineDisplay + idx).padStart(width, " ")} | ${line}`)
|
|
263
|
+
.join("\n");
|
|
264
|
+
}
|
|
265
|
+
if (truncation.truncatedBy === "lines") {
|
|
266
|
+
outputText = `${body}\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalLines}. Use offset=${nextOffset} to continue.]`;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
outputText = `${body}\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalLines} (${formatSize(truncation.maxBytes)} limit). Use offset=${nextOffset} to continue.]`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (userLimitedLines !== undefined &&
|
|
273
|
+
startLine + userLimitedLines < totalLines) {
|
|
274
|
+
endLineDisplay = startLine + userLimitedLines;
|
|
275
|
+
const remaining = totalLines - endLineDisplay;
|
|
276
|
+
const nextOffset = endLineDisplay + 1;
|
|
277
|
+
let body = truncation.content;
|
|
278
|
+
if (options?.lineNumbers) {
|
|
279
|
+
const lines = body.split("\n");
|
|
280
|
+
const width = String(endLineDisplay).length;
|
|
281
|
+
body = lines
|
|
282
|
+
.map((line, idx) => `${String(startLineDisplay + idx).padStart(width, " ")} | ${line}`)
|
|
283
|
+
.join("\n");
|
|
284
|
+
}
|
|
285
|
+
outputText = `${body}\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
endLineDisplay = totalLines;
|
|
289
|
+
let body = truncation.content;
|
|
290
|
+
if (options?.lineNumbers) {
|
|
291
|
+
const lines = body.split("\n");
|
|
292
|
+
const width = String(startLineDisplay + lines.length - 1).length;
|
|
293
|
+
body = lines
|
|
294
|
+
.map((line, idx) => `${String(startLineDisplay + idx).padStart(width, " ")} | ${line}`)
|
|
295
|
+
.join("\n");
|
|
296
|
+
}
|
|
297
|
+
outputText = body;
|
|
298
|
+
}
|
|
223
299
|
return {
|
|
224
300
|
file,
|
|
225
|
-
content:
|
|
301
|
+
content: outputText,
|
|
226
302
|
encoding: "utf-8",
|
|
303
|
+
totalLines,
|
|
304
|
+
startLine: startLineDisplay,
|
|
305
|
+
endLine: endLineDisplay,
|
|
306
|
+
truncation: truncation.truncated ? truncation : undefined,
|
|
227
307
|
};
|
|
228
308
|
}
|
|
229
309
|
const blob = await this.downloadFileBlob(projectId, file.id);
|
|
310
|
+
const mimeType = detectSupportedImageMimeType(file.name);
|
|
311
|
+
const isImage = Boolean(mimeType && mimeType.startsWith("image/"));
|
|
230
312
|
return {
|
|
231
313
|
file,
|
|
232
314
|
content: Buffer.from(blob).toString("base64"),
|
|
233
315
|
encoding: "base64",
|
|
316
|
+
mimeType,
|
|
317
|
+
isImage,
|
|
234
318
|
};
|
|
235
319
|
}
|
|
236
320
|
async pushYjsUpdateViaWebSocket(projectId, fileId, encodedUpdate, timeoutMs = 5000) {
|
|
@@ -342,61 +426,68 @@ export class TypstCloudClient {
|
|
|
342
426
|
return { syncChannel };
|
|
343
427
|
}
|
|
344
428
|
async writeFile(projectId, filePathOrId, content, options) {
|
|
345
|
-
const files = await this.listFiles(projectId);
|
|
346
429
|
const normalized = filePathOrId.replace(/^\/+/, "");
|
|
347
|
-
|
|
348
|
-
files
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
430
|
+
return withFileMutationQueue(`${projectId}:${normalized}`, async () => {
|
|
431
|
+
const files = await this.listFiles(projectId);
|
|
432
|
+
const existing = files.find((f) => f.id === filePathOrId) ??
|
|
433
|
+
files.find((f) => f.name === normalized || f.name === filePathOrId);
|
|
434
|
+
if (existing) {
|
|
435
|
+
if (existing.isMultiplayable) {
|
|
436
|
+
const remote = await this.downloadFileYjs(projectId, existing.id);
|
|
437
|
+
const { encodedUpdate } = replaceYjsText(remote, content);
|
|
438
|
+
const { syncChannel } = await this.saveYjsUpdate(projectId, existing.id, encodedUpdate, options?.preferWebSocket ?? true);
|
|
439
|
+
return {
|
|
440
|
+
file: existing,
|
|
441
|
+
created: false,
|
|
442
|
+
bytesWritten: Buffer.byteLength(content, "utf8"),
|
|
443
|
+
syncChannel,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const rawBytes = new TextEncoder().encode(content);
|
|
447
|
+
const { response } = await this.restCall("PUT", `files/${projectId}/${existing.id}`, { body: rawBytes });
|
|
448
|
+
const updatedMeta = (await response.json());
|
|
354
449
|
return {
|
|
355
|
-
file:
|
|
450
|
+
file: updatedMeta,
|
|
356
451
|
created: false,
|
|
357
|
-
bytesWritten:
|
|
358
|
-
syncChannel,
|
|
452
|
+
bytesWritten: rawBytes.byteLength,
|
|
453
|
+
syncChannel: "rest",
|
|
359
454
|
};
|
|
360
455
|
}
|
|
361
456
|
const rawBytes = new TextEncoder().encode(content);
|
|
362
|
-
const { response } = await this.restCall("
|
|
363
|
-
|
|
457
|
+
const { response } = await this.restCall("POST", `files/${projectId}`, {
|
|
458
|
+
queryParams: { name: normalized, convert: "false" },
|
|
459
|
+
body: rawBytes,
|
|
460
|
+
});
|
|
461
|
+
const body = (await response.json());
|
|
462
|
+
const createdMeta = unwrapSingleOrFirstFile(body);
|
|
364
463
|
return {
|
|
365
|
-
file:
|
|
366
|
-
created:
|
|
464
|
+
file: createdMeta,
|
|
465
|
+
created: true,
|
|
367
466
|
bytesWritten: rawBytes.byteLength,
|
|
368
467
|
syncChannel: "rest",
|
|
369
468
|
};
|
|
370
|
-
}
|
|
371
|
-
const rawBytes = new TextEncoder().encode(content);
|
|
372
|
-
const { response } = await this.restCall("POST", `files/${projectId}`, {
|
|
373
|
-
queryParams: { name: normalized, convert: "false" },
|
|
374
|
-
body: rawBytes,
|
|
375
469
|
});
|
|
376
|
-
const body = (await response.json());
|
|
377
|
-
const createdMeta = unwrapSingleOrFirstFile(body);
|
|
378
|
-
return {
|
|
379
|
-
file: createdMeta,
|
|
380
|
-
created: true,
|
|
381
|
-
bytesWritten: rawBytes.byteLength,
|
|
382
|
-
syncChannel: "rest",
|
|
383
|
-
};
|
|
384
470
|
}
|
|
385
471
|
async editFile(projectId, filePathOrId, edits, options) {
|
|
386
472
|
const file = await this.resolveFile(projectId, filePathOrId);
|
|
387
473
|
if (!file.isMultiplayable) {
|
|
388
474
|
throw new TypstLoadError(["NOT_MULTIPLAYABLE"], `File "${file.name}" is a binary blob and cannot be surgically text-edited`);
|
|
389
475
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
476
|
+
return withFileMutationQueue(`${projectId}:${file.id}`, async () => {
|
|
477
|
+
const remote = await this.downloadFileYjs(projectId, file.id);
|
|
478
|
+
const { encodedUpdate, previousText, updatedText, appliedCount, diff, patch, firstChangedLine, } = editYjsText(remote, edits, file.name);
|
|
479
|
+
const { syncChannel } = await this.saveYjsUpdate(projectId, file.id, encodedUpdate, options?.preferWebSocket ?? true);
|
|
480
|
+
return {
|
|
481
|
+
file,
|
|
482
|
+
appliedCount,
|
|
483
|
+
previousLength: previousText.length,
|
|
484
|
+
updatedLength: updatedText.length,
|
|
485
|
+
diff,
|
|
486
|
+
patch,
|
|
487
|
+
firstChangedLine,
|
|
488
|
+
syncChannel,
|
|
489
|
+
};
|
|
490
|
+
});
|
|
400
491
|
}
|
|
401
492
|
async uploadAsset(projectId, fileName, data, options) {
|
|
402
493
|
const normalized = fileName.replace(/^\/+/, "");
|
|
@@ -480,18 +571,92 @@ export class TypstCloudClient {
|
|
|
480
571
|
static parseShortDiagnostics(stderrText) {
|
|
481
572
|
return parseShortDiagnostics(stderrText);
|
|
482
573
|
}
|
|
483
|
-
async
|
|
484
|
-
const
|
|
574
|
+
async updateProject(projectId, updates) {
|
|
575
|
+
const res = await this.graphql(EDIT_PROJECT_MUTATION, {
|
|
576
|
+
projectId,
|
|
577
|
+
name: updates.name,
|
|
578
|
+
previewed: updates.previewed,
|
|
579
|
+
pinnedVersion: updates.pinnedVersion,
|
|
580
|
+
});
|
|
581
|
+
return res.updateProject;
|
|
582
|
+
}
|
|
583
|
+
async getWebDiagnostics(projectId, mainFile) {
|
|
584
|
+
const project = await this.getProject(projectId);
|
|
585
|
+
let effectiveMainFile = mainFile;
|
|
586
|
+
if (!effectiveMainFile) {
|
|
587
|
+
const previewedFile = project.files.find((f) => f.id === project.previewed);
|
|
588
|
+
effectiveMainFile = previewedFile?.name ?? "main.typ";
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
const requestedFile = project.files.find((f) => f.name === mainFile || f.id === mainFile);
|
|
592
|
+
if (requestedFile && requestedFile.id !== project.previewed) {
|
|
593
|
+
try {
|
|
594
|
+
await this.updateProject(projectId, { previewed: requestedFile.id });
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// If permission denied or mutation rejected, proceed anyway
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const fileContents = {};
|
|
602
|
+
for (const f of project.files) {
|
|
603
|
+
if (f.isMultiplayable) {
|
|
604
|
+
try {
|
|
605
|
+
const res = await this.readFile(projectId, f.name);
|
|
606
|
+
if (res.encoding === "utf-8" && typeof res.content === "string") {
|
|
607
|
+
fileContents[f.name] = res.content;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
// Ignore read failures for auxiliary files
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return captureWebDiagnostics({
|
|
616
|
+
appUrl: this.auth.appUrl,
|
|
617
|
+
projectId,
|
|
618
|
+
cookieHeader: this.auth.getCookieHeader(),
|
|
619
|
+
mainFile: effectiveMainFile,
|
|
620
|
+
fileContents,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async getDiagnostics(projectId, options) {
|
|
624
|
+
const opts = typeof options === "string" ? { mainFile: options } : options;
|
|
625
|
+
const mode = opts?.mode ?? "auto";
|
|
626
|
+
const requestedMainFile = opts?.mainFile;
|
|
627
|
+
if (mode === "web") {
|
|
628
|
+
return await this.getWebDiagnostics(projectId, requestedMainFile);
|
|
629
|
+
}
|
|
630
|
+
const typstAvailable = hasTypstBinary(this.typstBin);
|
|
631
|
+
if (!typstAvailable) {
|
|
632
|
+
if (mode === "cli") {
|
|
633
|
+
throw new TypstLoadError(["TYPST_BINARY_NOT_FOUND"], `Typst compiler binary '${this.typstBin}' was not found. Please install the Typst compiler (e.g. 'winget install Typst.Typst' / 'brew install typst') or use mode: 'web' for zero-binary cloud diagnostics.`);
|
|
634
|
+
}
|
|
635
|
+
return await this.getWebDiagnostics(projectId, requestedMainFile);
|
|
636
|
+
}
|
|
485
637
|
try {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
638
|
+
const { workspaceDir, mainFileName } = await this.materializeProjectWorkspace(projectId, requestedMainFile);
|
|
639
|
+
try {
|
|
640
|
+
const res = compileWorkspaceDiagnostics({
|
|
641
|
+
typstBin: this.typstBin,
|
|
642
|
+
projectId,
|
|
643
|
+
workspaceDir,
|
|
644
|
+
mainFileName,
|
|
645
|
+
});
|
|
646
|
+
return {
|
|
647
|
+
...res,
|
|
648
|
+
mode: "cli",
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
finally {
|
|
652
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
653
|
+
}
|
|
492
654
|
}
|
|
493
|
-
|
|
494
|
-
|
|
655
|
+
catch (err) {
|
|
656
|
+
if (mode === "auto") {
|
|
657
|
+
return await this.getWebDiagnostics(projectId, requestedMainFile);
|
|
658
|
+
}
|
|
659
|
+
throw err;
|
|
495
660
|
}
|
|
496
661
|
}
|
|
497
662
|
async exportPdf(projectId, options) {
|
|
@@ -542,4 +707,93 @@ export class TypstCloudClient {
|
|
|
542
707
|
rmSync(workspaceDir, { recursive: true, force: true });
|
|
543
708
|
}
|
|
544
709
|
}
|
|
710
|
+
async grepProject(projectId, options) {
|
|
711
|
+
const files = await this.listFiles(projectId);
|
|
712
|
+
const limit = options.limit ?? 100;
|
|
713
|
+
const allMatches = [];
|
|
714
|
+
let totalMatches = 0;
|
|
715
|
+
let filesSearched = 0;
|
|
716
|
+
for (const file of files) {
|
|
717
|
+
if (!file.isMultiplayable)
|
|
718
|
+
continue;
|
|
719
|
+
if (options.path) {
|
|
720
|
+
const normalizedPath = options.path.replace(/^\/+/, "");
|
|
721
|
+
if (file.name !== normalizedPath &&
|
|
722
|
+
!file.name.startsWith(`${normalizedPath}/`) &&
|
|
723
|
+
file.id !== options.path) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (options.glob && !matchGlob(file.name, options.glob)) {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
filesSearched++;
|
|
731
|
+
const yjsBytes = await this.downloadFileYjs(projectId, file.id);
|
|
732
|
+
const text = decodeYjsText(yjsBytes);
|
|
733
|
+
const remainingLimit = Math.max(0, limit - allMatches.length);
|
|
734
|
+
const result = searchInContent(file.name, text, {
|
|
735
|
+
pattern: options.pattern,
|
|
736
|
+
literal: options.literal,
|
|
737
|
+
ignoreCase: options.ignoreCase,
|
|
738
|
+
context: options.context,
|
|
739
|
+
limit: remainingLimit,
|
|
740
|
+
});
|
|
741
|
+
totalMatches += result.totalMatches;
|
|
742
|
+
allMatches.push(...result.matches);
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
matches: allMatches,
|
|
746
|
+
totalMatches,
|
|
747
|
+
filesSearched,
|
|
748
|
+
truncated: totalMatches > allMatches.length,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
async findAndReplace(projectId, options) {
|
|
752
|
+
const files = await this.listFiles(projectId);
|
|
753
|
+
const filesModified = [];
|
|
754
|
+
let totalReplacements = 0;
|
|
755
|
+
for (const file of files) {
|
|
756
|
+
if (!file.isMultiplayable)
|
|
757
|
+
continue;
|
|
758
|
+
if (options.filePath) {
|
|
759
|
+
const normalizedPath = options.filePath.replace(/^\/+/, "");
|
|
760
|
+
if (file.name !== normalizedPath && file.id !== options.filePath) {
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (options.glob && !matchGlob(file.name, options.glob)) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
await withFileMutationQueue(`${projectId}:${file.id}`, async () => {
|
|
768
|
+
const remote = await this.downloadFileYjs(projectId, file.id);
|
|
769
|
+
const text = decodeYjsText(remote);
|
|
770
|
+
const repResult = replaceInContent(file.name, text, {
|
|
771
|
+
pattern: options.pattern,
|
|
772
|
+
replacement: options.replacement,
|
|
773
|
+
regex: options.regex,
|
|
774
|
+
ignoreCase: options.ignoreCase,
|
|
775
|
+
limit: options.limit,
|
|
776
|
+
});
|
|
777
|
+
if (repResult.replacementCount > 0) {
|
|
778
|
+
if (!options.dryRun) {
|
|
779
|
+
const { encodedUpdate } = replaceYjsText(remote, repResult.updatedText);
|
|
780
|
+
await this.saveYjsUpdate(projectId, file.id, encodedUpdate, options.preferWebSocket ?? true);
|
|
781
|
+
}
|
|
782
|
+
filesModified.push({
|
|
783
|
+
file: file.name,
|
|
784
|
+
replacementsCount: repResult.replacementCount,
|
|
785
|
+
diff: repResult.diff,
|
|
786
|
+
patch: repResult.patch,
|
|
787
|
+
firstChangedLine: repResult.firstChangedLine,
|
|
788
|
+
});
|
|
789
|
+
totalReplacements += repResult.replacementCount;
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
filesModified,
|
|
795
|
+
totalReplacements,
|
|
796
|
+
dryRun: Boolean(options.dryRun),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
545
799
|
}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,34 @@ function resolveOptionalNumber(value) {
|
|
|
21
21
|
}
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
24
|
+
export function prepareEditArguments(input) {
|
|
25
|
+
if (!input)
|
|
26
|
+
return [];
|
|
27
|
+
if (Array.isArray(input)) {
|
|
28
|
+
return input.filter((e) => e && typeof e.oldText === "string" && typeof e.newText === "string");
|
|
29
|
+
}
|
|
30
|
+
if (typeof input === "string") {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(input);
|
|
33
|
+
if (Array.isArray(parsed)) {
|
|
34
|
+
return parsed.filter((e) => e && typeof e.oldText === "string" && typeof e.newText === "string");
|
|
35
|
+
}
|
|
36
|
+
if (parsed &&
|
|
37
|
+
typeof parsed.oldText === "string" &&
|
|
38
|
+
typeof parsed.newText === "string") {
|
|
39
|
+
return [parsed];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
}
|
|
44
|
+
if (typeof input === "object" && input !== null) {
|
|
45
|
+
const obj = input;
|
|
46
|
+
if (typeof obj.oldText === "string" && typeof obj.newText === "string") {
|
|
47
|
+
return [{ oldText: obj.oldText, newText: obj.newText }];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
24
52
|
export async function dispatchTypstTool(client, name, args = {}) {
|
|
25
53
|
switch (name) {
|
|
26
54
|
case "typst_auth_login":
|
|
@@ -51,15 +79,28 @@ export async function dispatchTypstTool(client, name, args = {}) {
|
|
|
51
79
|
case "typst_list_files":
|
|
52
80
|
return await client.listFiles(String(args.projectId));
|
|
53
81
|
case "typst_read_file":
|
|
54
|
-
return await client.readFile(String(args.projectId), String(args.filePath)
|
|
82
|
+
return await client.readFile(String(args.projectId), String(args.filePath), {
|
|
83
|
+
offset: resolveOptionalNumber(args.offset),
|
|
84
|
+
limit: resolveOptionalNumber(args.limit),
|
|
85
|
+
lineNumbers: typeof args.lineNumbers === "boolean"
|
|
86
|
+
? args.lineNumbers
|
|
87
|
+
: undefined,
|
|
88
|
+
});
|
|
55
89
|
case "typst_write_file":
|
|
56
90
|
return await client.writeFile(String(args.projectId), String(args.filePath), String(args.content), {
|
|
57
91
|
preferWebSocket: resolvePreferWebSocket(args.preferWebSocket),
|
|
58
92
|
});
|
|
59
|
-
case "typst_edit_file":
|
|
60
|
-
|
|
93
|
+
case "typst_edit_file": {
|
|
94
|
+
let edits = prepareEditArguments(args.edits);
|
|
95
|
+
if (edits.length === 0 &&
|
|
96
|
+
typeof args.oldText === "string" &&
|
|
97
|
+
typeof args.newText === "string") {
|
|
98
|
+
edits = [{ oldText: args.oldText, newText: args.newText }];
|
|
99
|
+
}
|
|
100
|
+
return await client.editFile(String(args.projectId), String(args.filePath), edits, {
|
|
61
101
|
preferWebSocket: resolvePreferWebSocket(args.preferWebSocket),
|
|
62
102
|
});
|
|
103
|
+
}
|
|
63
104
|
case "typst_upload_asset": {
|
|
64
105
|
let bytes;
|
|
65
106
|
if (typeof args.localFilePath === "string" && args.localFilePath) {
|
|
@@ -78,7 +119,10 @@ export async function dispatchTypstTool(client, name, args = {}) {
|
|
|
78
119
|
case "typst_rename_file":
|
|
79
120
|
return await client.renameFile(String(args.projectId), String(args.filePath), String(args.newName));
|
|
80
121
|
case "typst_get_diagnostics":
|
|
81
|
-
return await client.getDiagnostics(String(args.projectId),
|
|
122
|
+
return await client.getDiagnostics(String(args.projectId), {
|
|
123
|
+
mainFile: args.mainFile,
|
|
124
|
+
mode: args.mode,
|
|
125
|
+
});
|
|
82
126
|
case "typst_export_pdf":
|
|
83
127
|
return await client.exportPdf(String(args.projectId), {
|
|
84
128
|
mainFile: args.mainFile,
|
|
@@ -93,6 +137,28 @@ export async function dispatchTypstTool(client, name, args = {}) {
|
|
|
93
137
|
mode: args.mode ?? "pages",
|
|
94
138
|
includeBase64: Boolean(args.includeBase64),
|
|
95
139
|
});
|
|
140
|
+
case "typst_grep_project":
|
|
141
|
+
return await client.grepProject(String(args.projectId), {
|
|
142
|
+
pattern: String(args.pattern),
|
|
143
|
+
path: args.path,
|
|
144
|
+
glob: args.glob,
|
|
145
|
+
literal: Boolean(args.literal),
|
|
146
|
+
ignoreCase: Boolean(args.ignoreCase),
|
|
147
|
+
context: resolveOptionalNumber(args.context),
|
|
148
|
+
limit: resolveOptionalNumber(args.limit),
|
|
149
|
+
});
|
|
150
|
+
case "typst_find_and_replace":
|
|
151
|
+
return await client.findAndReplace(String(args.projectId), {
|
|
152
|
+
pattern: String(args.pattern),
|
|
153
|
+
replacement: String(args.replacement),
|
|
154
|
+
filePath: args.filePath,
|
|
155
|
+
glob: args.glob,
|
|
156
|
+
regex: Boolean(args.regex),
|
|
157
|
+
ignoreCase: Boolean(args.ignoreCase),
|
|
158
|
+
limit: resolveOptionalNumber(args.limit),
|
|
159
|
+
dryRun: Boolean(args.dryRun),
|
|
160
|
+
preferWebSocket: resolvePreferWebSocket(args.preferWebSocket),
|
|
161
|
+
});
|
|
96
162
|
default:
|
|
97
163
|
throw new Error(`Unknown tool: ${name}`);
|
|
98
164
|
}
|
|
@@ -101,7 +167,7 @@ export function createTypstMcpServer(options = {}) {
|
|
|
101
167
|
const client = new TypstCloudClient(options);
|
|
102
168
|
const server = new Server({
|
|
103
169
|
name: "typst-mcp",
|
|
104
|
-
version: "1.
|
|
170
|
+
version: "1.1.0",
|
|
105
171
|
}, {
|
|
106
172
|
capabilities: {
|
|
107
173
|
tools: {},
|
|
@@ -114,6 +180,32 @@ export function createTypstMcpServer(options = {}) {
|
|
|
114
180
|
const { name, arguments: args } = request.params;
|
|
115
181
|
try {
|
|
116
182
|
const result = await dispatchTypstTool(client, name, args ?? {});
|
|
183
|
+
if (name === "typst_read_file" &&
|
|
184
|
+
typeof result === "object" &&
|
|
185
|
+
result !== null &&
|
|
186
|
+
Boolean(result.isImage) &&
|
|
187
|
+
typeof result.mimeType === "string" &&
|
|
188
|
+
typeof result.content === "string") {
|
|
189
|
+
const img = result;
|
|
190
|
+
return {
|
|
191
|
+
content: [
|
|
192
|
+
{
|
|
193
|
+
type: "text",
|
|
194
|
+
text: JSON.stringify({
|
|
195
|
+
file: img.file,
|
|
196
|
+
encoding: img.encoding,
|
|
197
|
+
mimeType: img.mimeType,
|
|
198
|
+
isImage: true,
|
|
199
|
+
}, null, 2),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: "image",
|
|
203
|
+
data: img.content,
|
|
204
|
+
mimeType: img.mimeType,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
117
209
|
return {
|
|
118
210
|
content: [
|
|
119
211
|
{
|