glosc-mcp 1.1.0 → 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 CHANGED
@@ -35,4 +35,16 @@
35
35
  - 文本写入/编辑:按行添加/替换/删除(可批量),或创建/替换/删除整个文件
36
36
  - 入参(概要):
37
37
  - 整文件:`{ path, file: { action: "create"|"replace"|"delete", content?, overwrite? }, encoding?, newline?, ensureFinalNewline?, returnContent? }`
38
- - 按行:`{ path, edits: [{ op: "add"|"replace"|"delete", ... }], createIfMissing?, encoding?, newline?, ensureFinalNewline?, returnContent? }`
38
+ - 按行:`{ path, edits: [{ op: "add"|"replace"|"delete", ... }], createIfMissing?, encoding?, newline?, ensureFinalNewline?, returnContent? }`
39
+
40
+ - `renameFile`
41
+ - 重命名文件(同目录改名)
42
+ - 入参:`{ path: string, newName: string, overwrite?: boolean }`
43
+
44
+ - `moveFile`
45
+ - 移动文件到新路径(必要时自动创建目录;目标为目录时会移动到该目录下)
46
+ - 入参:`{ from: string, to: string, overwrite?: boolean, createDirs?: boolean }`
47
+
48
+ - `listFilesRecursive`
49
+ - 递归获取文件夹中的所有文件(默认最多返回 5000 条,避免输出过大)
50
+ - 入参:`{ dir: string, limit?: number }`
package/build/GloscMcp.js CHANGED
@@ -178,122 +178,106 @@ export class GloscMcp {
178
178
  ],
179
179
  };
180
180
  });
181
- this.server.registerTool("editText", {
182
- description: "文本写入/编辑:支持按行添加/替换/删除(可批量),或创建/替换/删除整个文件",
183
- inputSchema: z
184
- .object({
185
- path: z.string().describe("文件的绝对路径"),
186
- encoding: z
181
+ this.server.registerTool("renameFile", {
182
+ description: "重命名文件(同目录改名)",
183
+ inputSchema: z.object({
184
+ path: z.string().describe("源文件的绝对路径"),
185
+ newName: z
187
186
  .string()
188
- .describe("文本编码(默认 utf8)")
189
- .default("utf8"),
190
- newline: z
191
- .enum(["auto", "lf", "crlf"])
192
- .describe("换行符策略:auto/lf/crlf")
193
- .default("auto"),
194
- ensureFinalNewline: z
187
+ .describe("新文件名(仅文件名,不含路径)"),
188
+ overwrite: z
195
189
  .boolean()
196
- .describe("是否确保文件末尾以换行结尾")
190
+ .describe("目标已存在时是否覆盖")
197
191
  .default(false),
198
- returnContent: z
192
+ }),
193
+ }, async ({ path, newName, overwrite }) => {
194
+ try {
195
+ const res = await GloscTools.renameFile({
196
+ path,
197
+ newName,
198
+ overwrite,
199
+ });
200
+ return {
201
+ content: [
202
+ {
203
+ type: "text",
204
+ text: JSON.stringify(res, null, 2),
205
+ },
206
+ ],
207
+ };
208
+ }
209
+ catch (error) {
210
+ const errorMessage = error instanceof Error ? error.message : String(error);
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text",
215
+ text: `重命名失败: ${errorMessage}`,
216
+ },
217
+ ],
218
+ };
219
+ }
220
+ });
221
+ this.server.registerTool("moveFile", {
222
+ description: "移动文件到新路径(必要时自动创建目录)",
223
+ inputSchema: z.object({
224
+ from: z.string().describe("源文件的绝对路径"),
225
+ to: z.string().describe("目标路径(文件路径或目录)"),
226
+ overwrite: z
199
227
  .boolean()
200
- .describe("是否在结果中返回最终内容(可能很大)")
228
+ .describe("目标已存在时是否覆盖")
201
229
  .default(false),
202
- createIfMissing: z
230
+ createDirs: z
203
231
  .boolean()
204
- .describe("按行 edits 时文件不存在是否自动新建(默认 false)")
205
- .default(false),
206
- file: z
207
- .union([
208
- z.object({
209
- action: z
210
- .literal("create")
211
- .describe("新建文件"),
212
- content: z.string().describe("文件内容"),
213
- overwrite: z
214
- .boolean()
215
- .describe("文件已存在时是否覆盖")
216
- .default(false),
217
- }),
218
- z.object({
219
- action: z
220
- .literal("replace")
221
- .describe("替换整个文件内容"),
222
- content: z.string().describe("文件内容"),
223
- }),
224
- z.object({
225
- action: z
226
- .literal("delete")
227
- .describe("删除文件"),
228
- }),
229
- ])
230
- .optional(),
231
- edits: z
232
- .array(z.union([
233
- z.object({
234
- op: z.literal("add"),
235
- at: z
236
- .number()
237
- .int()
238
- .min(1)
239
- .describe("插入位置行号(1-based);允许 lineCount+1 表示追加"),
240
- position: z
241
- .enum(["before", "after"])
242
- .describe("插入到该行之前或之后")
243
- .default("before"),
244
- lines: z
245
- .array(z.string())
246
- .min(1)
247
- .describe("要插入的行(不含换行符)"),
248
- }),
249
- z.object({
250
- op: z.literal("replace"),
251
- start: z
252
- .number()
253
- .int()
254
- .min(1)
255
- .describe("起始行号(1-based)"),
256
- end: z
257
- .number()
258
- .int()
259
- .min(1)
260
- .optional()
261
- .describe("结束行号(1-based,含);缺省则等于 start"),
262
- lines: z
263
- .array(z.string())
264
- .describe("替换后的行(可多行)"),
265
- }),
266
- z.object({
267
- op: z.literal("delete"),
268
- start: z
269
- .number()
270
- .int()
271
- .min(1)
272
- .describe("起始行号(1-based)"),
273
- end: z
274
- .number()
275
- .int()
276
- .min(1)
277
- .optional()
278
- .describe("结束行号(1-based,含);缺省则等于 start"),
279
- }),
280
- ]))
281
- .optional(),
282
- })
283
- .refine((v) => !!v.file !== !!v.edits, {
284
- message: "必须且只能提供 file 或 edits 之一",
232
+ .describe("是否自动创建目标目录")
233
+ .default(true),
285
234
  }),
286
- }, async (input) => {
235
+ }, async ({ from, to, overwrite, createDirs }) => {
236
+ try {
237
+ const res = await GloscTools.moveFile({
238
+ from,
239
+ to,
240
+ overwrite,
241
+ createDirs,
242
+ });
243
+ return {
244
+ content: [
245
+ {
246
+ type: "text",
247
+ text: JSON.stringify(res, null, 2),
248
+ },
249
+ ],
250
+ };
251
+ }
252
+ catch (error) {
253
+ const errorMessage = error instanceof Error ? error.message : String(error);
254
+ return {
255
+ content: [
256
+ {
257
+ type: "text",
258
+ text: `移动失败: ${errorMessage}`,
259
+ },
260
+ ],
261
+ };
262
+ }
263
+ });
264
+ this.server.registerTool("listFilesRecursive", {
265
+ description: "递归获取文件夹中的所有文件",
266
+ inputSchema: z.object({
267
+ dir: z.string().describe("目录的绝对路径"),
268
+ limit: z
269
+ .number()
270
+ .int()
271
+ .min(1)
272
+ .max(20000)
273
+ .describe("最多返回多少条(防止输出过大)")
274
+ .default(5000),
275
+ }),
276
+ }, async ({ dir, limit }) => {
287
277
  try {
288
- const res = await GloscTools.editTextFile({
289
- path: input.path,
290
- encoding: input.encoding,
291
- newline: input.newline,
292
- ensureFinalNewline: input.ensureFinalNewline,
293
- returnContent: input.returnContent,
294
- createIfMissing: input.createIfMissing,
295
- file: input.file,
296
- edits: input.edits,
278
+ const res = await GloscTools.listFilesRecursive({
279
+ dir,
280
+ limit,
297
281
  });
298
282
  return {
299
283
  content: [
@@ -310,7 +294,7 @@ export class GloscMcp {
310
294
  content: [
311
295
  {
312
296
  type: "text",
313
- text: `编辑文本失败: ${errorMessage}`,
297
+ text: `列出文件失败: ${errorMessage}`,
314
298
  },
315
299
  ],
316
300
  };
@@ -508,184 +508,154 @@ if ($null -ne $apps) {
508
508
  // 支持 \n / \r\n,且保留末尾空行(比如文件以换行结尾)
509
509
  if (text === "")
510
510
  return [""];
511
- const lines = text.split(/\r?\n/);
512
- return lines;
511
+ return text.split(/\r?\n/);
513
512
  }
514
513
  static joinLines(lines, newline) {
515
514
  return lines.join(newline);
516
515
  }
517
- static async editTextFile(options) {
518
- const filePath = options.path?.trim();
519
- if (!filePath)
520
- throw new Error("path 不能为空");
521
- const encoding = options.encoding ?? "utf8";
522
- const returnContent = options.returnContent ?? false;
523
- const createIfMissing = options.createIfMissing ?? false;
524
- const hasFileAction = !!options.file;
525
- const hasEdits = Array.isArray(options.edits) && options.edits.length > 0;
526
- if ((hasFileAction && hasEdits) || (!hasFileAction && !hasEdits)) {
527
- throw new Error("必须且只能提供 file 或 edits 之一");
528
- }
529
- let existedBefore = true;
530
- let originalBuffer = null;
516
+ static async pathExists(p) {
531
517
  try {
532
- originalBuffer = await fs.readFile(filePath);
518
+ await fs.stat(p);
519
+ return true;
533
520
  }
534
521
  catch (e) {
522
+ if (e && (e.code === "ENOENT" || e.code === "ENOTDIR"))
523
+ return false;
524
+ throw e;
525
+ }
526
+ }
527
+ static async removePath(targetPath) {
528
+ const st = await fs.lstat(targetPath);
529
+ if (st.isDirectory()) {
530
+ await fs.rm(targetPath, { recursive: true, force: true });
531
+ }
532
+ else {
533
+ await fs.unlink(targetPath);
534
+ }
535
+ }
536
+ static async movePathInternal(options) {
537
+ const from = options.from?.trim();
538
+ const to = options.to?.trim();
539
+ if (!from)
540
+ throw new Error("from 不能为空");
541
+ if (!to)
542
+ throw new Error("to 不能为空");
543
+ const overwrite = options.overwrite ?? false;
544
+ const createDirs = options.createDirs ?? true;
545
+ const fromStat = await fs.lstat(from).catch((e) => {
535
546
  if (e && (e.code === "ENOENT" || e.code === "ENOTDIR")) {
536
- existedBefore = false;
537
- originalBuffer = null;
538
- }
539
- else {
540
- throw e;
547
+ throw new Error("源路径不存在");
541
548
  }
549
+ throw e;
550
+ });
551
+ let dest = to;
552
+ const toLooksLikeDir = /[\\/]+$/.test(to);
553
+ if (toLooksLikeDir) {
554
+ dest = path.join(to, path.basename(from));
542
555
  }
543
- if (options.file) {
544
- const action = options.file.action;
545
- if (action === "delete") {
546
- if (existedBefore) {
547
- await fs.unlink(filePath);
556
+ else {
557
+ try {
558
+ const toStat = await fs.lstat(to);
559
+ if (toStat.isDirectory()) {
560
+ dest = path.join(to, path.basename(from));
548
561
  }
549
- return {
550
- ok: true,
551
- path: filePath,
552
- action: "delete",
553
- existedBefore,
554
- };
555
562
  }
556
- if (action === "create") {
557
- if (existedBefore && !options.file.overwrite) {
558
- throw new Error("文件已存在,action=create 且 overwrite=false");
563
+ catch (e) {
564
+ if (!(e && (e.code === "ENOENT" || e.code === "ENOTDIR"))) {
565
+ throw e;
559
566
  }
560
- await fs.mkdir(path.dirname(filePath), { recursive: true });
561
- const newline = GloscTools.normalizeNewlineOption(options.newline, undefined);
562
- let content = options.file.content ?? "";
563
- if (options.ensureFinalNewline) {
564
- if (!content.endsWith("\n") && !content.endsWith("\r\n")) {
565
- content += newline;
566
- }
567
- }
568
- const outBuffer = iconv.encode(content, encoding);
569
- await fs.writeFile(filePath, outBuffer);
570
- return {
571
- ok: true,
572
- path: filePath,
573
- action: existedBefore ? "replace" : "create",
574
- existedBefore,
575
- bytesWritten: outBuffer.length,
576
- content: returnContent ? content : undefined,
577
- };
578
567
  }
579
- // replace
580
- await fs.mkdir(path.dirname(filePath), { recursive: true });
581
- const decodedExisting = originalBuffer != null
582
- ? iconv.decode(originalBuffer, encoding)
583
- : undefined;
584
- const newline = GloscTools.normalizeNewlineOption(options.newline, decodedExisting);
585
- let content = options.file.content ?? "";
586
- if (options.ensureFinalNewline) {
587
- if (!content.endsWith("\n") && !content.endsWith("\r\n")) {
588
- content += newline;
589
- }
568
+ }
569
+ if (await GloscTools.pathExists(dest)) {
570
+ if (!overwrite) {
571
+ throw new Error("目标已存在;如需覆盖请设置 overwrite=true");
590
572
  }
591
- const outBuffer = iconv.encode(content, encoding);
592
- await fs.writeFile(filePath, outBuffer);
593
- return {
594
- ok: true,
595
- path: filePath,
596
- action: "replace",
597
- existedBefore,
598
- bytesWritten: outBuffer.length,
599
- content: returnContent ? content : undefined,
600
- };
573
+ await GloscTools.removePath(dest);
601
574
  }
602
- // line edits
603
- let text = "";
604
- if (originalBuffer != null) {
605
- text = iconv.decode(originalBuffer, encoding);
575
+ if (createDirs) {
576
+ await fs.mkdir(path.dirname(dest), { recursive: true });
606
577
  }
607
- else {
608
- if (!createIfMissing) {
609
- throw new Error("文件不存在;如需新建请使用 file.action=create createIfMissing=true");
610
- }
611
- await fs.mkdir(path.dirname(filePath), { recursive: true });
612
- text = "";
613
- }
614
- const newline = GloscTools.normalizeNewlineOption(options.newline, text);
615
- // 注意:split 后如果原文为空,返回 [""],这样 lineCount 语义更贴近编辑器
616
- let lines = GloscTools.splitLinesPreserveEmptyEnd(text);
617
- const lineCountBefore = lines.length;
618
- const edits = options.edits ?? [];
619
- for (const edit of edits) {
620
- if (edit.op === "add") {
621
- const position = edit.position ?? "before";
622
- const at = Math.trunc(edit.at);
623
- if (!Number.isFinite(at) || at < 1) {
624
- throw new Error("add.at 必须为 >=1 的整数");
625
- }
626
- // 允许 at = lines.length + 1(before)用于追加到末尾
627
- const maxAt = lines.length + 1;
628
- if (at > maxAt) {
629
- throw new Error(`add.at 超出范围:当前最大允许 ${maxAt}(可用 lineCount+1 追加)`);
578
+ try {
579
+ await fs.rename(from, dest);
580
+ return { ok: true, from, to: dest };
581
+ }
582
+ catch (e) {
583
+ // 跨盘符/设备移动在部分平台会抛 EXDEV
584
+ if (e && e.code === "EXDEV") {
585
+ if (fromStat.isDirectory()) {
586
+ const cp = fs.cp;
587
+ if (!cp) {
588
+ throw new Error("跨设备移动目录失败(EXDEV),且当前 Node 版本不支持 fs.cp");
589
+ }
590
+ await cp(from, dest, { recursive: true, force: overwrite });
591
+ await fs.rm(from, { recursive: true, force: true });
592
+ return { ok: true, from, to: dest };
630
593
  }
631
- const insertIndex = position === "after"
632
- ? Math.min(at, lines.length) // after 最多插在最后一行之后
633
- : at - 1;
634
- lines.splice(insertIndex, 0, ...edit.lines);
594
+ await fs.copyFile(from, dest);
595
+ await fs.unlink(from);
596
+ return { ok: true, from, to: dest };
635
597
  }
636
- else if (edit.op === "replace") {
637
- const start = Math.trunc(edit.start);
638
- const end = Math.trunc(edit.end ?? edit.start);
639
- if (!Number.isFinite(start) || start < 1) {
640
- throw new Error("replace.start 必须为 >=1 的整数");
641
- }
642
- if (!Number.isFinite(end) || end < start) {
643
- throw new Error("replace.end 必须为 >= start 的整数");
644
- }
645
- if (end > lines.length) {
646
- throw new Error(`replace 超出范围:当前行数 ${lines.length}`);
647
- }
648
- lines.splice(start - 1, end - start + 1, ...edit.lines);
598
+ throw e;
599
+ }
600
+ }
601
+ static async renameFile(options) {
602
+ const from = options.path?.trim();
603
+ const newName = options.newName?.trim();
604
+ if (!from)
605
+ throw new Error("path 不能为空");
606
+ if (!newName)
607
+ throw new Error("newName 不能为空");
608
+ // 限制为“同目录改名”,避免把 rename move 用
609
+ const base = path.basename(newName);
610
+ if (base !== newName) {
611
+ throw new Error("newName 只能是文件名,不能包含路径分隔符");
612
+ }
613
+ const to = path.join(path.dirname(from), newName);
614
+ return GloscTools.movePathInternal({
615
+ from,
616
+ to,
617
+ overwrite: options.overwrite,
618
+ createDirs: true,
619
+ });
620
+ }
621
+ static async moveFile(options) {
622
+ return GloscTools.movePathInternal(options);
623
+ }
624
+ static async listFilesRecursive(options) {
625
+ const dir = options.dir?.trim();
626
+ if (!dir)
627
+ throw new Error("dir 不能为空");
628
+ const limit = Math.max(1, Math.trunc(options.limit ?? 5000));
629
+ const rootStat = await fs.lstat(dir).catch((e) => {
630
+ if (e && (e.code === "ENOENT" || e.code === "ENOTDIR")) {
631
+ throw new Error("目录不存在");
649
632
  }
650
- else if (edit.op === "delete") {
651
- const start = Math.trunc(edit.start);
652
- const end = Math.trunc(edit.end ?? edit.start);
653
- if (!Number.isFinite(start) || start < 1) {
654
- throw new Error("delete.start 必须为 >=1 的整数");
655
- }
656
- if (!Number.isFinite(end) || end < start) {
657
- throw new Error("delete.end 必须为 >= start 的整数");
633
+ throw e;
634
+ });
635
+ if (!rootStat.isDirectory()) {
636
+ throw new Error("dir 必须是目录");
637
+ }
638
+ const files = [];
639
+ const stack = [dir];
640
+ let truncated = false;
641
+ while (stack.length > 0) {
642
+ const current = stack.pop();
643
+ const entries = await fs.readdir(current, { withFileTypes: true });
644
+ for (const entry of entries) {
645
+ const full = path.join(current, entry.name);
646
+ if (entry.isDirectory()) {
647
+ stack.push(full);
658
648
  }
659
- if (end > lines.length) {
660
- throw new Error(`delete 超出范围:当前行数 ${lines.length}`);
649
+ else if (entry.isFile()) {
650
+ files.push(full);
651
+ if (files.length >= limit) {
652
+ truncated = true;
653
+ stack.length = 0;
654
+ break;
655
+ }
661
656
  }
662
- lines.splice(start - 1, end - start + 1);
663
- if (lines.length === 0)
664
- lines = [""];
665
- }
666
- else {
667
- const neverEdit = edit;
668
- throw new Error(`未知 edit.op: ${neverEdit.op}`);
669
- }
670
- }
671
- let outText = GloscTools.joinLines(lines, newline);
672
- if (options.ensureFinalNewline) {
673
- if (!outText.endsWith("\n") && !outText.endsWith("\r\n")) {
674
- outText += newline;
675
657
  }
676
658
  }
677
- const outBuffer = iconv.encode(outText, encoding);
678
- await fs.writeFile(filePath, outBuffer);
679
- return {
680
- ok: true,
681
- path: filePath,
682
- action: "edit",
683
- existedBefore,
684
- lineCountBefore,
685
- lineCountAfter: lines.length,
686
- editsApplied: edits.length,
687
- bytesWritten: outBuffer.length,
688
- content: returnContent ? outText : undefined,
689
- };
659
+ return { ok: true, dir, files, truncated };
690
660
  }
691
661
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glosc-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,11 +34,9 @@
34
34
  "@modelcontextprotocol/sdk": "^1.25.1",
35
35
  "cheerio": "^1.1.2",
36
36
  "csv-parser": "^3.2.0",
37
- "iconv-lite": "^0.7.1",
38
37
  "mammoth": "^1.11.0",
39
38
  "pdf-parse": "^2.4.5",
40
39
  "playwright": "^1.57.0",
41
- "sharp": "^0.34.5",
42
40
  "xlsx": "^0.18.5",
43
41
  "zod": "^4.2.1"
44
42
  },