mcp-read-file-server 1.5.0 → 1.6.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 (4) hide show
  1. package/README.md +4 -2
  2. package/SKILL.md +17 -5
  3. package/index.js +151 -61
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -182,7 +182,7 @@ claude mcp list
182
182
  | `read_files` | 多次 Read | 批量读取多个文件明文(数组或逗号分隔字符串) | `paths` |
183
183
  | `read_file_partial` | Read(局部) | 局部读取文件(前N字符 / 指定行范围) | `path`、`mode`、`charCount`、`startLine`、`endLine` |
184
184
  | `write_file` | Write | 写入文件(支持追加模式 / 行尾风格 / BOM 保留) | `path`、`content`、`mode`、`eol` |
185
- | `edit_file` | Edit/MultiEdit | 精确字符串/正则替换后写回(CRLF/LF 自动兼容、BOM 保留、正则默认多行模式) | `path`、`oldString`、`newString`、`useRegex`、`replaceAll`、`ignoreCase` |
185
+ | `edit_file` | Edit/MultiEdit | 精确替换后写回(CRLF/LF 自动兼容、BOM 保留、正则多行模式、`edits` 批量原子编辑、失败附相似行诊断) | `path`、`oldString`、`newString`、`edits`、`useRegex`、`replaceAll`、`ignoreCase` |
186
186
  | `search_files` | Grep | 递归搜索文件内容(支持 `**` 目录通配、跳过二进制/超大文件) | `pattern`、`path`、`include`、`exclude`、`ignoreCase`、`onlyMatching`、`maxResults` |
187
187
  | `find_files` | Glob | 按文件名 glob 递归查找(如 `**/*.test.js`) | `pattern`、`path`、`maxResults` |
188
188
  | `list_directory` | LS | 列出目录内容(类型/大小/时间) | `path`、`showHidden` |
@@ -220,8 +220,10 @@ Windows 下文件多为 CRLF 换行,而 AI Agent 生成的多行 `oldString`
220
220
  - **提示信息**:触发换行适配时,返回结果会附 `ℹ️ 换行符已自动适配` 说明,方便排查
221
221
  - **BOM 自动处理**:UTF-8 BOM 读取时自动剥离、写回时自动补回,`oldString` 无需关心 BOM
222
222
  - **正则模式默认多行**:`useRegex=true` 时自动附加 `m` 标志,`^xxx` / `xxx$` 按行锚定
223
+ - **批量原子编辑(edits 数组)**:一次调用完成多处修改,按序应用;**任一条目失败则整体不写盘**,不会产生「半改状态」。条目按文件现状顺序构造(前面条目的结果参与后续条目匹配)
224
+ - **失败附相似行诊断**:字符串匹配失败时返回「可能相关的行」及相似度,直接对照排查空白/缩进差异,无需盲目重试
223
225
 
224
- 注意:该兼容仅针对换行符差异,空格、缩进等其他空白字符仍需与原文完全一致。
226
+ 注意:该兼容仅针对换行符差异,空格、缩进等其他空白字符仍需与原文完全一致。含反引号 `` ` `` 与 `${}` 的内容直接原样传参(JSON 传输无 JS 模板字面量转义问题)。
225
227
 
226
228
  ### 其他内置保护
227
229
 
package/SKILL.md CHANGED
@@ -85,8 +85,10 @@ description: 在文件加密软件(天锐绿盾 / IP-Guard / 亿赛通 / 深
85
85
  ```
86
86
  1. mcp__read-file-server__read_file 读取明文
87
87
  2. 分析内容
88
- 3. mcp__read-file-server__edit_file 修改
89
- - oldString 必须从第 1 步读到的内容里**原样复制**(含空格、缩进;换行风格差异会自动兼容)
88
+ 3. 修改:
89
+ - 单处修改 -> mcp__read-file-server__edit_file(oldString/newString)
90
+ - 多处修改 -> mcp__read-file-server__edit_file 的 edits 数组(原子:失败整体不写盘)
91
+ - oldString 从第 1 步读到的内容里**原样复制**(含空格、缩进;换行风格差异会自动兼容)
90
92
  4. 必要时再 read_file 验证修改结果
91
93
  ```
92
94
 
@@ -120,17 +122,27 @@ description: 在文件加密软件(天锐绿盾 / IP-Guard / 亿赛通 / 深
120
122
  | 参数 | 类型 | 必填 | 默认 | 说明 |
121
123
  |------|------|------|------|------|
122
124
  | `path` | string | ✅ | - | 文件绝对路径 |
123
- | `oldString` | string | | - | 要替换的原内容,必须**精确匹配** |
124
- | `newString` | string | | - | 替换后的新内容 |
125
- | `useRegex` | boolean | | false | true 时 oldString 当正则,可用 `$1 $2` 引用捕获组 |
125
+ | `oldString` | string | 单次模式✅ | - | 要替换的原内容,必须**精确匹配**(换行风格差异已自动兼容) |
126
+ | `newString` | string | 单次模式✅ | - | 替换后的新内容 |
127
+ | `edits` | array | 批量模式✅ | - | 批量原子编辑:`[{oldString, newString, replaceAll?}]` 按序应用,**任一条目失败则整体不写盘**(不会产生半改状态)。一次完成多处修改必须用它,不要逐条调用 |
128
+ | `useRegex` | boolean | ❌ | false | true 时 oldString 当正则(单次模式),可用 `$1 $2` 引用捕获组;默认启用多行模式(`^`/`$` 按行锚定) |
126
129
  | `replaceAll` | boolean | ❌ | false | true 时替换所有匹配项;false 时仅替换第一处 |
127
130
  | `ignoreCase` | boolean | ❌ | false | 是否忽略大小写(仅字符串模式生效) |
128
131
 
129
132
  ### 常见用法
130
133
  - **单点替换**:`useRegex=false, replaceAll=false`(默认)
131
134
  - **批量替换**:`useRegex=true, replaceAll=true`(如改命名)
135
+ - **多处修改**:`edits` 数组(如重命名+改值+删行一次完成,失败自动整体回滚)
132
136
  - **正则提取后重组**:`useRegex=true, newString` 里用 `$1` `$2`
133
137
 
138
+ ### 实战避坑(来自真实使用反馈)
139
+
140
+ 1. **换行差异已自动兼容,无需关心 CRLF/LF**:oldString 用 LF 匹配 CRLF 文件(或相反)均可命中,newString 行尾自动跟随文件风格。**不要**再为此绕道写 Node 补丁脚本手工归一
141
+ 2. **oldString 含反引号 `` ` `` 与 `${}` 直接原样传入**:MCP 参数走 JSON 传输,无 JS 模板字面量的转义层级问题;同样不要绕道脚本(脚本里转义极易写错)
142
+ 3. **多处修改必须用 `edits` 批量模式**:逐条调用时若中途失败,前面条目已写盘会产生「半改状态」,后续按原内容构造的 oldString 必然失配;`edits` 原子模式要么全成要么不动。**批量条目按序应用**:前面条目的 newString 会成为后续条目的匹配环境,请按文件现状顺序构造(如 A 改为 B 后,后条可用 B 做锚点)
143
+ 4. **oldString 带足上下文保证唯一**:短 oldString 命中多处时工具会警告(如 `替换 1/2 处`),此时加长上下文(含前后行)唯一定位;超长行(如记忆表格行)优先选行内独有片段做锚点
144
+ 5. **匹配失败看诊断**:失败信息会附「可能相关的行」及相似度,直接对照检查空白/缩进/字符差异,不要盲目重试;正则模式报错时注意 oldString 正则里 `$` 需写成 `\$`(如匹配字面 `$1`),而 newString 里的 `$1` 是捕获组引用原样保留
145
+
134
146
  ---
135
147
 
136
148
  ## 六、注意事项
package/index.js CHANGED
@@ -475,26 +475,119 @@ function eolInsensitiveReplace(original, oldString, replacement, replaceAll, ign
475
475
  return { updated, count: positions.length };
476
476
  }
477
477
 
478
+ /**
479
+ * 匹配失败时的相似行诊断:在文件中找出与 oldString 首行(或整体)最相似的行,
480
+ * 输出行号与原文,帮助 Agent 快速定位「看起来一样实际差个空白/字符」的位置。
481
+ * 诊断方式:按公共 8-gram 计数相似度(无依赖的轻量实现),返回最多 3 条候选。
482
+ */
483
+ function diagnoseNoMatch(original, oldString) {
484
+ // 取 oldString 的首行做行级比对(多数失败源于首行定位错误)
485
+ const firstLine = oldString.split(/\r?\n/)[0].trim();
486
+ if (!firstLine || firstLine.length < 6) return "";
487
+ const grams = new Set();
488
+ const N = 8;
489
+ for (let i = 0; i + N <= firstLine.length; i++) grams.add(firstLine.slice(i, i + N));
490
+ if (!grams.size) return "";
491
+ const lines = original.split(/\r?\n/);
492
+ let best = [];
493
+ for (let i = 0; i < lines.length; i++) {
494
+ const line = lines[i];
495
+ if (!line.trim()) continue;
496
+ let hit = 0;
497
+ for (let j = 0; j + N <= line.length; j++) {
498
+ if (grams.has(line.slice(j, j + N))) hit++;
499
+ }
500
+ const denom = Math.max(grams.size, Math.ceil(line.trim().length / N));
501
+ const sim = hit / denom;
502
+ if (sim > 0.25) best.push({ line: i + 1, sim, text: line.trim().slice(0, 120) });
503
+ }
504
+ best.sort((a, b) => b.sim - a.sim);
505
+ best = best.slice(0, 3);
506
+ if (!best.length) return "";
507
+ return "\n可能相关的行(相似度排序,请对照检查空白/缩进/字符差异):\n" +
508
+ best.map((b) => " 第 " + b.line + " 行 (相似度 " + Math.round(b.sim * 100) + "%): " + b.text).join("\n");
509
+ }
510
+
511
+ /**
512
+ * 对文本应用一次替换(edit_file 单次模式与批量模式的每个条目共用)。
513
+ * 两级匹配:①字节级精确匹配 ②换行归一化兜底(CRLF/LF 差异)。
514
+ * 返回 { ok, updated, count, eolAdapted, error, diagnose };失败时 error 带原因,
515
+ * diagnose 带相似行诊断(仅字符串模式且完全无命中时提供)。
516
+ */
517
+ function applySingleEdit(original, oldString, newString, useRegex, replaceAll, ignoreCase) {
518
+ // 替换文本统一按文件主导行尾风格写入(调用方已归一 newString)
519
+ let matcher = null;
520
+ if (useRegex) {
521
+ try {
522
+ // 正则模式默认附加 m 标志:^/$ 按行锚定(Agent 常用行级正则习惯),JS 无内联标志无法由调用方自行开启
523
+ matcher = new RegExp(oldString, ((replaceAll ? "g" : "") + "m" + (ignoreCase ? "i" : "")));
524
+ } catch (e) {
525
+ return { ok: false, error: "正则表达式无效: " + e.message };
526
+ }
527
+ }
528
+ if (useRegex) {
529
+ // 计数与替换均带 m 标志,保持与构造 matcher 时一致
530
+ const globalMatcher = new RegExp(matcher.source, "gm" + (ignoreCase ? "i" : ""));
531
+ const matches = original.match(globalMatcher);
532
+ const count = matches ? matches.length : 0;
533
+ if (count === 0) return { ok: false, error: "正则未匹配到内容", count: 0 };
534
+ const replaceMatcher = replaceAll ? globalMatcher : new RegExp(matcher.source, "m" + (ignoreCase ? "i" : ""));
535
+ return { ok: true, updated: original.replace(replaceMatcher, newString), count, eolAdapted: false };
536
+ }
537
+ if (oldString === "") {
538
+ return { ok: false, error: "oldString 不能为空字符串" };
539
+ }
540
+ // 第一优先:原样精确匹配(字节级一致,最安全)
541
+ const hay = ignoreCase ? original.toLowerCase() : original;
542
+ const needle = ignoreCase ? oldString.toLowerCase() : oldString;
543
+ let idx = 0, exactCount = 0;
544
+ while ((idx = hay.indexOf(needle, idx)) !== -1) { exactCount++; idx += needle.length; }
545
+ if (exactCount > 0) {
546
+ let updated;
547
+ if (replaceAll) {
548
+ const esc = oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
549
+ // 用函数替换避免 newString 中的 $& $1 等被误解析为替换模式
550
+ updated = original.replace(new RegExp(esc, ignoreCase ? "gi" : "g"), () => newString);
551
+ } else {
552
+ const pos = hay.indexOf(needle);
553
+ updated = original.slice(0, pos) + newString + original.slice(pos + oldString.length);
554
+ }
555
+ return { ok: true, updated, count: exactCount, eolAdapted: false };
556
+ }
557
+ // 第二优先:换行符兼容匹配。文件是 CRLF 而 oldString 用 LF(或相反)时,
558
+ // 字节级比对必然失败,归一换行后再匹配,命中即记为换行适配替换
559
+ const adapted = eolInsensitiveReplace(original, oldString, newString, replaceAll, ignoreCase);
560
+ if (adapted) {
561
+ return { ok: true, updated: adapted.updated, count: adapted.count, eolAdapted: true };
562
+ }
563
+ return { ok: false, error: "未找到匹配内容", count: 0, diagnose: diagnoseNoMatch(original, oldString) };
564
+ }
565
+
478
566
  // 注册 edit_file 工具(精确替换,替代受加密影响的 Edit/MultiEdit)
479
567
  server.tool(
480
568
  "edit_file",
481
- "对文件内容做精确字符串或正则替换后写回(明文)。字符串匹配自动兼容 CRLF/LF 换行差异,替换文本行尾自动跟随文件主导风格。加密软件环境下内置 Edit/MultiEdit 直写会破坏加密,本工具用 Node.js fs 读改写,自动加密落盘。替代内置 Edit/MultiEdit 工具。",
569
+ "对文件内容做精确字符串或正则替换后写回(明文)。支持两种形态:①单次替换(oldString/newString);②批量原子编辑(edits 数组,按序应用,任一条目失败则整体不写盘,避免半改状态)。字符串匹配自动兼容 CRLF/LF 换行差异,替换文本行尾自动跟随文件主导风格。加密软件环境下内置 Edit/MultiEdit 直写会破坏加密,本工具用 Node.js fs 读改写,自动加密落盘。替代内置 Edit/MultiEdit 工具。",
482
570
  {
483
571
  path: z.string().describe("文件路径,支持相对路径或绝对路径"),
484
- oldString: z.string().describe("要被替换的原字符串。useRegex=true 时作为正则表达式"),
485
- newString: z.string().describe("替换后的字符串。正则模式下可用 $1 $2 等捕获组引用"),
486
- useRegex: z.boolean().optional().describe("是否将 oldString 当作正则表达式,默认 false(纯字符串匹配)"),
487
- replaceAll: z.boolean().optional().describe("是否替换全部匹配项,默认 false 仅替换第一处"),
572
+ oldString: z.string().optional().describe("单次模式必填:要被替换的原字符串。useRegex=true 时作为正则表达式"),
573
+ newString: z.string().optional().describe("单次模式必填:替换后的字符串。正则模式下可用 $1 $2 等捕获组引用"),
574
+ edits: z.array(z.object({
575
+ oldString: z.string().describe("要被替换的原字符串(批量条目,不支持正则)"),
576
+ newString: z.string().describe("替换后的字符串(批量条目)"),
577
+ replaceAll: z.boolean().optional().describe("是否替换全部匹配项,默认 false 仅替换第一处"),
578
+ })).optional().describe("批量原子编辑模式:多个替换按序应用,任一条目未匹配则整体不写盘。适合一次完成多处修改(替代 MultiEdit)"),
579
+ useRegex: z.boolean().optional().describe("单次模式:是否将 oldString 当作正则表达式,默认 false(纯字符串匹配)"),
580
+ replaceAll: z.boolean().optional().describe("单次模式:是否替换全部匹配项,默认 false 仅替换第一处"),
488
581
  ignoreCase: z.boolean().optional().describe("是否忽略大小写,默认 false。仅在非正则的字符串模式下生效"),
489
582
  },
490
- async ({ path: filePath, oldString, newString, useRegex, replaceAll, ignoreCase }) => {
583
+ async ({ path: filePath, oldString, newString, edits, useRegex, replaceAll, ignoreCase }) => {
491
584
  try {
492
585
  // readFileContent 会剥离 BOM 并记录,写回时补回,避免 oldString 匹配首行失败
493
586
  const readResult = readFileContent(filePath);
494
587
  if (!readResult.ok) {
495
588
  return { content: [{ type: "text", text: "❌ " + readResult.error }], isError: true };
496
589
  }
497
- const original = readResult.content;
590
+ let original = readResult.content;
498
591
  const hasBom = readResult.hasBom;
499
592
  // 非 UTF-8 防护:GBK 等编码按 UTF-8 读入会产生大量 U+FFFD,此时做任何替换再写回,
500
593
  // 原始字节信息会永久丢失(不可逆损坏)。检测到即拒绝编辑并明确提示。
@@ -504,70 +597,67 @@ server.tool(
504
597
  isError: true,
505
598
  };
506
599
  }
507
- // 文件主导换行风格:CRLF 文件写入的替换文本也转成 CRLF,保持文件风格统一
508
- const fileEol = detectEol(original);
509
- const normalizedNew = normalizeEol(newString, fileEol);
510
- let matcher;
511
- if (useRegex) {
512
- try {
513
- // 正则模式默认附加 m 标志:^/$ 按行锚定(Agent 常用行级正则习惯),JS 无内联标志无法由调用方自行开启
514
- const flags = (replaceAll ? "g" : "") + "m" + (ignoreCase ? "i" : "");
515
- matcher = new RegExp(oldString, flags);
516
- } catch (e) {
517
- return { content: [{ type: "text", text: "❌ 正则表达式无效: " + e.message }], isError: true };
518
- }
600
+ // 形态分派:edits 数组走批量原子模式;否则要求 oldString/newString 成对
601
+ const isBatch = Array.isArray(edits) && edits.length > 0;
602
+ if (!isBatch && (oldString === undefined || newString === undefined)) {
603
+ return { content: [{ type: "text", text: "❌ 参数缺失:请提供 oldString+newString(单次替换),或 edits 数组(批量原子编辑)" }], isError: true };
519
604
  }
520
- let count;
521
- let updated;
522
- let eolAdapted = false; // 是否触发了换行符兼容替换
523
- if (useRegex) {
524
- // 计数与替换均带 m 标志,保持与构造 matcher 时一致
525
- const globalMatcher = new RegExp(matcher.source, "gm" + (ignoreCase ? "i" : ""));
526
- const matches = original.match(globalMatcher);
527
- count = matches ? matches.length : 0;
528
- const replaceMatcher = replaceAll ? globalMatcher : new RegExp(matcher.source, "m" + (ignoreCase ? "i" : ""));
529
- updated = original.replace(replaceMatcher, normalizedNew);
530
- } else {
531
- if (oldString === "") {
532
- return { content: [{ type: "text", text: "❌ oldString 不能为空字符串" }], isError: true };
533
- }
534
- // 第一优先:原样精确匹配(字节级一致,最安全)
535
- let idx = 0, c = 0;
536
- const hay = ignoreCase ? original.toLowerCase() : original;
537
- const needle = ignoreCase ? oldString.toLowerCase() : oldString;
538
- while ((idx = hay.indexOf(needle, idx)) !== -1) { c++; idx += needle.length; }
539
- count = c;
540
- if (count > 0) {
541
- if (replaceAll) {
542
- const esc = oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
543
- // 用函数替换避免 newString 中的 $& $1 等被误解析为替换模式
544
- updated = original.replace(new RegExp(esc, ignoreCase ? "gi" : "g"), () => normalizedNew);
545
- } else {
546
- const pos = hay.indexOf(needle);
547
- updated = original.slice(0, pos) + normalizedNew + original.slice(pos + oldString.length);
605
+ // 文件主导换行风格:所有替换文本统一转成该风格(CRLF 文件写入 CRLF,保持风格统一)
606
+ const fileEol = detectEol(original);
607
+ const toFileEol = (s) => normalizeEol(s, fileEol);
608
+
609
+ // ---------- 批量原子模式:先全部校验再统一写盘,杜绝半改状态 ----------
610
+ if (isBatch) {
611
+ let content = original;
612
+ let total = 0;
613
+ const applied = [];
614
+ // 全部条目按序在内存中应用;任一失败立即返回,文件保持原样
615
+ for (let i = 0; i < edits.length; i++) {
616
+ const item = edits[i];
617
+ const r = applySingleEdit(content, item.oldString, toFileEol(item.newString), false, item.replaceAll === true, false);
618
+ if (!r.ok) {
619
+ return {
620
+ content: [{
621
+ type: "text",
622
+ text: "❌ 批量编辑第 " + (i + 1) + "/" + edits.length + " 条失败: " + r.error + "\n已中止,文件未做任何修改(原子模式,前面条目也不会写入): " + filePath +
623
+ (r.diagnose || "") +
624
+ "\n提示:批量条目按顺序应用,前面条目的 newString 会改变后续条目的匹配环境,请按文件现状顺序构造。",
625
+ }],
626
+ isError: true,
627
+ };
548
628
  }
549
- } else {
550
- // 第二优先:换行符兼容匹配。文件是 CRLF 而 oldString 用 LF(或相反)时,
551
- // 字节级比对必然失败,这里归一换行后再匹配,命中即记为换行适配替换
552
- const adapted = eolInsensitiveReplace(original, oldString, normalizedNew, replaceAll, ignoreCase);
553
- if (adapted) {
554
- count = adapted.count;
555
- updated = adapted.updated;
556
- eolAdapted = true;
557
- } else {
558
- count = 0;
559
- updated = original;
629
+ if (r.updated === content) {
630
+ return {
631
+ content: [{ type: "text", text: "❌ 批量编辑第 " + (i + 1) + "/" + edits.length + " 条替换后内容无变化(newString 与原文相同),已中止: " + filePath }],
632
+ isError: true,
633
+ };
560
634
  }
635
+ content = r.updated;
636
+ total += r.count;
637
+ applied.push("#" + (i + 1) + " 替换 " + (item.replaceAll === true ? r.count : 1) + "/" + r.count + " 处");
561
638
  }
639
+ if (content === original) {
640
+ return { content: [{ type: "text", text: "⚠️ 批量编辑应用后内容无变化,文件未修改: " + filePath }] };
641
+ }
642
+ fs.writeFileSync(filePath, (hasBom ? "\uFEFF" : "") + content, "utf-8");
643
+ return {
644
+ content: [{ type: "text", text: "✅ 批量编辑成功: " + filePath + "\n共 " + edits.length + " 条," + total + " 处替换\n" + applied.join("\n") }],
645
+ };
562
646
  }
563
- if (count === 0) {
647
+
648
+ // ---------- 单次模式(原行为) ----------
649
+ const normalizedNew = toFileEol(newString);
650
+ const r = applySingleEdit(original, oldString, normalizedNew, useRegex === true, replaceAll === true, ignoreCase === true);
651
+ if (!r.ok) {
564
652
  return {
565
- content: [{ type: "text", text: "⚠️ 未找到匹配内容,文件未修改。请检查 oldString(或正则)是否正确: " + filePath + "\n提示:若内容本身无误,请确认文件与 oldString 的换行风格(CRLF/LF)及空白字符是否一致。" }],
653
+ content: [{ type: "text", text: "⚠️ " + r.error + ",文件未修改。请检查 oldString(或正则)是否正确: " + filePath + (r.diagnose || "") + "\n提示:换行风格(CRLF/LF)已自动兼容;重点检查空格、缩进、字符是否与原文完全一致。" }],
566
654
  isError: true,
567
655
  };
568
656
  }
657
+ const count = r.count;
658
+ const updated = r.updated;
569
659
  let warning = "";
570
- if (eolAdapted) {
660
+ if (r.eolAdapted) {
571
661
  warning = "\nℹ️ 换行符已自动适配:oldString 与文件换行风格不一致(CRLF/LF),已按换行归一化匹配完成替换。";
572
662
  }
573
663
  if (!replaceAll && count > 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-read-file-server",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "加密环境文件操作 MCP 工具。当 Node.js 是加密软件白名单进程时,通过 fs 模块自动解密读写文件明文,替代 AI Agent 内置文件工具,适用于任何支持 MCP 协议的 Agent。",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",