dsh-rule-engine 0.5.2 → 0.5.4

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.
@@ -9,6 +9,7 @@ import {
9
9
  DSH_KEYWORDS_RE,
10
10
  INLINE_CMD,
11
11
  MANUAL_PATH_RE,
12
+ PROTECTED_FILENAME_RE,
12
13
  SKILL_EXEMPT,
13
14
  commandText,
14
15
  isAssemblyMutationTool,
@@ -40,12 +41,11 @@ const RULE_HINTS = {
40
41
  "1": "先分析根因,确认问题后再继续",
41
42
  "9": "改用脚本文件或显式 UTF-8 BOM 流程",
42
43
  "12A": "先 ask_user_question 获取匹配授权",
43
- "12D": "先 ask_user_question 获取匹配授权",
44
44
  "13A": "先对目标路径执行备份(复制到 .bak/.backups/trash-)",
45
45
  "18": "先读取 ~/.dsh/skills/dsh-usage-manual/SKILL.md",
46
46
  "21": "按规则 21 分级确认后再落盘",
47
+ "22": "先回答/展示方案,待用户明确授权(非疑问句+动作词,或 ask 授权答复)后再执行",
47
48
  "24": "确认插件 dsh.bundle 类型或改用正确挂载",
48
- "25": "将该工具纳入统一守卫覆盖",
49
49
  "27": "先运行 node scripts/audit-mount-consistency.mjs --profile web"
50
50
  };
51
51
 
@@ -80,6 +80,11 @@ export function isProfilePackageJson(p) {
80
80
  return typeof p === "string" && /profiles[\\/][^\\/]+[\\/]package\.json$/i.test(p);
81
81
  }
82
82
 
83
+ /** 判定命令是否"整条命令仅调用统一入口 dsh-manual-write.mjs"(无 ; | & 链式/换行) */
84
+ function isEntryChannelCommand(cmd) {
85
+ return typeof cmd === "string" && /^[^\n;|&]*dsh-manual-write\.mjs[^\n;|&]*$/i.test(cmd);
86
+ }
87
+
83
88
  /** 计算 edit/write/str_replace 后的目标文件内容;无法可靠计算时返回 null */
84
89
  function resultingFileContent(name, args) {
85
90
  const p = pathTarget(args);
@@ -226,6 +231,20 @@ export function guardDecision(state, exec, now = Date.now()) {
226
231
  );
227
232
  }
228
233
 
234
+ // 阶段 C:硬拦 pwsh/bash 绕过统一入口直接写受保护文件(规则 19⑧/21⑨ 的机器执行层)
235
+ // 合法通道 = 整条命令仅调用 dsh-manual-write.mjs(无 ; | & 链式/换行,防注释文本伪造放行)
236
+ if (!unlock && (name === "pwsh" || name === "bash")) {
237
+ const cmd = commandText(args) || "";
238
+ if (cmd && /(?:set-content|out-file|add-content|write|copy-item|move-item|rename-item|writefilesync|fs\.writefilesync)/i.test(cmd) && PROTECTED_FILENAME_RE.test(cmd)) {
239
+ if (!isEntryChannelCommand(cmd)) {
240
+ return makeHit(
241
+ { ruleId: "__self-protect", title: "受保护文件禁止绕过统一入口直写", action: "deny" },
242
+ `【硬拦截】受保护文件禁止通过 pwsh/bash 绕过统一入口直写;请使用 scripts/dsh-manual-write.mjs(整个命令只能调用该脚本,不得链式拼接其他写命令;或 /guard unlock 临时放行)。`
243
+ );
244
+ }
245
+ }
246
+ }
247
+
229
248
  // 写前版本校验(建议③):版本化文件(SKILL.md/AGENTS.md/CHANGELOG/README 等)在写入前
230
249
  // 用 old/new 模拟结果做校验,不合规直接拒绝——避免"先写后回滚"的副作用与假成功
231
250
  // 位置在自护之后:受保护文件需先 unlock(用户明确授权)再接受版本校验
@@ -262,6 +281,17 @@ function matchRule(cfg, ctx) {
262
281
  const { name, args, p, cmd, session, unlock, state, now } = ctx;
263
282
  const id = String(cfg.ruleId);
264
283
 
284
+ // 规则 22⑦ 机器化:用户消息是疑问句 → 本回合禁止变更类工具调用
285
+ //("你的执行方案难道没问题吗?"虽含"执行"仍是疑问句——同形词不构成指令,不豁免)
286
+ if (cfg.handler === "rule22-7-direct") {
287
+ if (session.turn.questionOnly) {
288
+ if (isReadOnlyTool(name, args)) return null; // 只读 → 豁免③
289
+ if (SAFE_UNCOVERED_TOOLS.has(name)) return null; // 展示/授权/查询类(含 ask_user_question)→ 豁免③
290
+ return makeHit(cfg, `【硬拦截】用户消息是疑问句而非授权(规则 22⑦):请先回答/展示方案,待用户明确授权后再执行(放行:非疑问句+动作词,或 ask_user_question 授权答复)`);
291
+ }
292
+ return null;
293
+ }
294
+
265
295
  // 规则 1:同工具同参数连续失败 ≥2 次后拦第 3 次(失败计数由 tool/result 更新)
266
296
  if (cfg.handler === "rule1-retry") {
267
297
  const key = `${name}:${JSON.stringify(args || {})}`;
@@ -301,6 +331,9 @@ function matchRule(cfg, ctx) {
301
331
 
302
332
  // 规则 13A:删除/覆盖/高风险写前需有“目标路径对应备份”证据
303
333
  if (cfg.handler === "rule13a-backup") {
334
+ // 统一入口命令豁免:dsh-manual-write.mjs 每次写入前自身执行备份(backup() 保留 5 份),
335
+ // 引擎静态扫描看不到脚本内部动作(已知盲区);入口命令也已被 __self-protect 限定为唯一写通道。
336
+ if ((name === "pwsh" || name === "bash") && isEntryChannelCommand(cmd)) return null;
304
337
  const destructive = (name === "pwsh" || name === "bash") && cmd && (DESTRUCTIVE_CMD.test(cmd) || (isSensitiveToolCall(name, args) && !/git\s+(push|commit)/i.test(cmd)));
305
338
  const highRiskWrite = (name === "edit" || name === "write" || (name === "str_replace_editor" && args?.command !== "view")) && isProtectedConfigPath(p);
306
339
  if (highRiskWrite && unlock && !isHighRiskEntryFile(p)) return null;
@@ -351,8 +384,8 @@ function matchRule(cfg, ctx) {
351
384
  return null;
352
385
  }
353
386
 
354
- // 规则 12A/12D:敏感操作需要匹配授权证据
355
- if (cfg.handler === "rule12a-approval" || cfg.handler === "rule12d-sensitive") {
387
+ // 规则 12A:敏感操作需要匹配授权证据
388
+ if (cfg.handler === "rule12a-approval") {
356
389
  if (isSensitiveToolCall(name, args)) {
357
390
  // 规则 19:dsh-usage-manual/SKILL.md 正文更新免逐次确认(仅手册本身)
358
391
  if (p && MANUAL_PATH_RE.test(p)) return null;
@@ -379,7 +412,7 @@ function matchRule(cfg, ctx) {
379
412
  return null;
380
413
  }
381
414
 
382
- // 规则 24:插件装配类型确认(A 硬拦)
415
+ // 规则 24:插件装配类型确认(A 硬拦)+ 变更类工具统一覆盖(原规则 25 语义,检查④)
383
416
  if (cfg.handler === "rule24-assembly-type") {
384
417
  if (name === "dev_install_package") {
385
418
  const dir = args?.dir;
@@ -399,16 +432,13 @@ function matchRule(cfg, ctx) {
399
432
  if (badBundles && badBundles.length) {
400
433
  return makeHit(cfg, `【硬拦截】${p} 的 dsh.profile.bundles 包含非 bundle/无法确认类型:${badBundles.join(";")}(规则 24)`);
401
434
  }
402
- return null;
403
- }
404
-
405
- // 规则 25:插件变更类工具统一守卫覆盖(A 硬拦;未覆盖的变更工具拒绝)
406
- if (cfg.handler === "rule25-tool-coverage") {
435
+ // 规则 24④:所有能产生文件写入/删除/移动效果的工具都必须纳入统一守卫——
436
+ // 未覆盖的变更类工具运行时直接拒绝(防"改个新工具就绕过守卫")
407
437
  if (isReadOnlyTool(name, args)) return null;
408
438
  if (COVERED_MUTATION_TOOLS.has(name)) return null;
409
439
  if (SAFE_UNCOVERED_TOOLS.has(name)) return null;
410
440
  if (looksLikeFileMutation(name, args)) {
411
- return makeHit(cfg, `【硬拦截】未覆盖的变更类工具 ${name},违反规则 25:请先纳入统一守卫覆盖`);
441
+ return makeHit(cfg, `【硬拦截】未覆盖的变更类工具 ${name},违反规则 24④:请先纳入统一守卫覆盖`);
412
442
  }
413
443
  return null;
414
444
  }
@@ -69,10 +69,24 @@ async function callLlm(ctx, route, rule) {
69
69
  }
70
70
 
71
71
  /**
72
- * 对非 high 置信规则做一次 LLM 增量理解。
73
- * @param {object} ctx Cordis context
74
- * @param {object} state 插件状态(configs 会被原地更新)
72
+ * LLM 结果消毒(防软化/误强化,2026-08-23):
73
+ * - confidence:只允许提高(low→medium/high、medium→high);不允许降低(防"模型把规则软化到不硬拦")
74
+ * - hints:只允许追加(辅助检测面,不缩小)
75
+ * - actions / handler:一律不受 LLM 影响——执行语义只由模式库(HANDLER_BY_RULE/等级)决定,
76
+ * 既是防软化也是防"LLM 把规则误指派到无关 handler 造成误拦"。
75
77
  */
78
+ const CONFIDENCE_RANK = { low: 0, medium: 1, high: 2 };
79
+
80
+ export function sanitizeLlmResult(result, cfg) {
81
+ const out = {};
82
+ if (typeof result?.confidence === "string" && CONFIDENCE_RANK[result.confidence] > CONFIDENCE_RANK[cfg?.confidence || "low"]) {
83
+ out.confidence = result.confidence;
84
+ }
85
+ if (Array.isArray(result?.hints) && result.hints.length > 0) {
86
+ out.hints = [...new Set([...(cfg?.hints || []), ...result.hints])];
87
+ }
88
+ return out;
89
+ }
76
90
  export async function enrichRulesWithLlm(ctx, state) {
77
91
  if (!ctx?.llm || !Array.isArray(state.configs)) return;
78
92
  const route = await resolveRoute(ctx);
@@ -91,11 +105,10 @@ export async function enrichRulesWithLlm(ctx, state) {
91
105
  try {
92
106
  const result = await callLlm(ctx, route, cfg);
93
107
  if (result && result.confidence) {
94
- if (Array.isArray(result.actions)) cfg.actions = result.actions;
95
- if (typeof result.confidence === "string") cfg.confidence = result.confidence;
96
- if (typeof result.handler === "string") cfg.handler = result.handler;
97
- if (Array.isArray(result.hints)) cfg.hints = result.hints;
98
- cfg.llmEnriched = true;
108
+ const patch = sanitizeLlmResult(result, cfg);
109
+ if (patch.confidence) cfg.confidence = patch.confidence;
110
+ if (patch.hints) cfg.hints = patch.hints;
111
+ if (patch.confidence || patch.hints) cfg.llmEnriched = true;
99
112
  }
100
113
  } catch {
101
114
  // 单条失败不影响其他规则,保留模式库结果
@@ -74,7 +74,8 @@ export function loadRules() {
74
74
 
75
75
  function finalize(rule, lines) {
76
76
  const body = lines.slice(rule.startLine + 1, rule.endLine).join("\n").trim();
77
- const levelMatch = rule.title.match(/执行等级[::]\s*([A-DM+]+(?:\s*[强弱])?)/);
77
+ // 等级正则:支持空格/顿号连接的组合等级(如 "B + D"、"A+ D"),解析结果去空白
78
+ const levelMatch = rule.title.match(/执行等级[::]\s*([A-DM+](?:\s*[强弱])?(?:\s*[++、]\s*[A-DM+](?:\s*[强弱])?)*)/);
78
79
  return {
79
80
  index: rule.index,
80
81
  title: rule.title,
@@ -33,6 +33,10 @@ export const PROMISE_WORDS =
33
33
 
34
34
  export const URL_RE = /https?:\/\/[^\s]+/i;
35
35
 
36
+ /** 回环/内网地址模式(规则 12C B 级留痕用) */
37
+ export const PRIVATE_NETWORK_RE =
38
+ /(?:127\.0\.0\.1|localhost|169\.254\.169\.254|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})/i;
39
+
36
40
  export const SOURCE_MARK = /来源|出处|via|source|reference|引自|参考/i;
37
41
 
38
42
  export const CJK_RE = /[\u4e00-\u9fff]/;
@@ -28,15 +28,17 @@ const SELF_CERT_HINTS = {
28
28
  "22": { re: /我记下了|记住了|收到,我记下了|放心,我记住了/i, reason: "检测到「我记下了」类空话;正确动作是落盘执行并汇报" },
29
29
  "23": { re: /完成|交付/, reason: "交付/完成声明未附运行时验证证据" },
30
30
  "6": { re: /(?:API|JSON|正则|异步|依赖注入|回调|schema|DI)/i, reason: "请按规则 6 用零基础用户能懂的语言解释" },
31
- "16": { re: /建议|优化|更优方案/, reason: "请按规则 16 用五要素格式提建议,避免频繁打断" },
32
- "20": { re: /推理档位|off|high|max/, reason: "请按规则 20 给出档位建议并说明理由" },
31
+ "16": { re: /建议|优化|更优方案|推理档位|档位/, reason: "请按规则 16 用绑定检查格式提建议(或给档位建议并说明理由),避免频繁打断" },
33
32
  "12E": { re: /重启|GUI|登录|界面|窗口/, reason: "请按规则 12E 先确认界面真实存在并声明能力边界" },
34
33
  "26": { re: /(?=发布|Release|附件)(?!(?:Attach binaries|正式 Asset|release\.assets))/i, reason: "请按规则 26 确认正式 Release Asset(release.assets)并附 tgz/zip" },
35
34
  "10": { re: /新会话|旧会话|旧项目|上下文恢复/, reason: "请按规则 10 自证:如实说明默认不自动继承,按优先级恢复并标注来源" },
36
35
  "12C": { re: /下载|网络|curl|Clash|被墙/, reason: "请按规则 12C 自证:需代理先告知、失败即停、下载后校验、排查先测连通性" },
37
36
  "13B": { re: /会话文件|会话修复|fromRestore|Inbox 回放|孤儿扫描/, reason: "请按规则 13B 自证:会话替换前用户完全退出、三层验证一次跑完、mtime 判断" },
38
37
  "15": { re: /文件版本|文件用途|新旧|版本.*处置|保留文件/, reason: "请按规则 15 自证:不凭 mtime/文件名判断新旧,无法确定版本/用途时保留并询问" },
39
- "19": { re: /学到新.*DSH|踩到新坑|踩坑.*沉淀|手册有误|知识必沉淀/, reason: "请按规则 19 自证:按①-⑦沉淀并当次汇报正文同步位置" }
38
+ "19": { re: /学到新.*DSH|踩到新坑|踩坑.*沉淀|手册有误|知识必沉淀/, reason: "请按规则 19 自证:按①-⑧沉淀并当次汇报正文同步位置" },
39
+ "28": { re: /新建.*文件|放入.*目录|工作区.*归类/, reason: "请按规则 28 自证:新文件按工作区目录索引归类,不确定则查 README 或放 _inbox" },
40
+ "29": { re: /依赖闭包|node_modules.*验证|动过.*node_modules/, reason: "请按规则 29 自证:重启前验证依赖闭包(关键包非空/顶层 junction 完整/无 dangling)" },
41
+ "30": { re: /变更验证|验证证据|不破坏.*依赖/, reason: "请按规则 30 自证:执行前证明不破坏依赖+已授权,完成后附验证证据" }
40
42
  };
41
43
 
42
44
  /** 简单判断一段文本是否以英文为主 */
@@ -21,28 +21,51 @@ const HANDLER_BY_RULE = {
21
21
  7: "rule7-promise",
22
22
  9: "rule9-inline-bom",
23
23
  11: "rule11-language",
24
- 12: "rule12-approval",
25
24
  "12A": "rule12a-approval",
26
25
  "12B": "rule12b-skill",
27
26
  "12C": "rule12c-network",
28
- "12D": "rule12d-sensitive",
29
- 13: "rule13-backup",
30
27
  "13A": "rule13a-backup",
31
28
  "13B": "rule13b-session",
32
29
  14: "rule14-report",
33
30
  18: "rule18-manual-first",
34
31
  21: "rule21-meta",
35
- 22: "rule22-direct",
32
+ 22: "rule22-7-direct",
36
33
  23: "rule23-runtime-verify",
37
34
  24: "rule24-assembly-type",
38
- 25: "rule25-tool-coverage",
39
35
  26: "rule26-release-asset",
40
36
  27: "rule27-mount-audit"
41
37
  };
42
38
 
43
- /** 根据执行等级推导动作 */
39
+ /**
40
+ * 覆盖自省(一致性预防机制):
41
+ * - dead:HANDLER_BY_RULE 映射了但 AGENTS.md 无此规则 ID(死映射,应清理)
42
+ * - uncovered:A/C/M(deny/ask/meta)级但无 handler 的规则(规则声称会被机器执行,实际不会)
43
+ * 在 /guard status 与一致性测试/脚本中使用;规则 21④/19⑦ 的体检触发条件之一。
44
+ */
45
+ export function analyzeCoverage(configs) {
46
+ const dead = new Map(); // ruleId -> handler(映射存在但规则不存在)
47
+ const uncovered = []; // A/C/M 级规则但引擎无执行 handler
48
+ const present = new Set();
49
+ for (const cfg of configs || []) {
50
+ if (cfg && cfg.ruleId !== undefined && cfg.ruleId !== null) present.add(String(cfg.ruleId));
51
+ }
52
+ for (const [id, handler] of Object.entries(HANDLER_BY_RULE)) {
53
+ if (!present.has(String(id))) dead.set(String(id), handler);
54
+ }
55
+ for (const cfg of configs || []) {
56
+ if (cfg.disabled) continue;
57
+ const acts = cfg.actions || [];
58
+ const hard = acts.some((a) => a === "deny" || a === "ask" || a === "meta");
59
+ if (hard && !cfg.handler) {
60
+ uncovered.push({ ruleId: String(cfg.ruleId), title: cfg.title, actions: acts });
61
+ }
62
+ }
63
+ return { dead, uncovered };
64
+ }
65
+
66
+ /** 根据执行等级推导动作(容忍空白/强弱/连接符:如 "B + D"、"A 弱 + D"、"D 强") */
44
67
  export function actionsForLevel(level) {
45
- const s = String(level || "").toUpperCase();
68
+ const s = String(level || "").toUpperCase().replace(/\s+/g, "");
46
69
  const actions = [];
47
70
  if (s.includes("A")) actions.push("deny");
48
71
  if (s.includes("B")) actions.push("correct");
package/lib/index.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  isBackupTool,
21
21
  isGetDateCommand,
22
22
  isManualReadTool,
23
+ PRIVATE_NETWORK_RE,
23
24
  setWorkspaceRoot
24
25
  } from "./core/patterns.js";
25
26
  import {
@@ -65,6 +66,7 @@ import { isVersionedFile, validateEditedFile } from "./core/version-guard.js";
65
66
  import { computeMountSignature, profileNameFromArgs } from "./core/mount-signature.js";
66
67
  import { detectSilentError, extractToolOutput } from "./core/silent-error.js";
67
68
  import { enrichRulesWithLlm } from "./core/llm-understander.js";
69
+ import { analyzeCoverage } from "./core/understander.js";
68
70
 
69
71
  export const name = "dsh-rule-engine";
70
72
  export const inject = ["tools", "commands", "agents", "workspaceRegistry", "skills", "llm"];
@@ -548,12 +550,15 @@ async function executeGuard(ctx, invocation) {
548
550
  const high = state.configs.filter((c) => c.confidence === "high").length;
549
551
  const medium = state.configs.filter((c) => c.confidence === "medium").length;
550
552
  const low = state.configs.filter((c) => c.confidence === "low").length;
553
+ // 覆盖自省(一致性预防):dead = 映射有但规则无;uncovered = 硬等级规则但无 handler
554
+ const { dead, uncovered } = analyzeCoverage(state.configs);
551
555
  const parts = [
552
556
  "【规则引擎状态】",
553
557
  ` 总开关:${state.enabled ? "开启" : "已关闭"}`,
554
558
  ` 任务契约:${state.taskContract?.taskContractEnabled ? `开启(模式 ${state.taskContract.taskContractMode}|ask ${state.taskContract.askEnabled ? "开" : "关"})` : "关闭"}`,
555
559
  ` 规则容器:${state.configOk ? `正常(${state.configs.length} 条规则)` : "⚠ " + state.configError}`,
556
560
  ` 理解置信度:high ${high} / medium ${medium} / low ${low}`,
561
+ ` 覆盖自省:${dead.size === 0 && uncovered.length === 0 ? "无缺口" : (dead.size > 0 ? `死映射 ${[...dead.keys()].join("、")}(引擎映射存在但规则已删除,建议清理)` : "") + (uncovered.length > 0 ? `;未覆盖 ${uncovered.map((u) => u.ruleId).join("、")}(${uncovered.map((u) => u.actions.join("+")).join(";")} 但无 handler,规则声明强制但引擎不执行,建议降 D 级或补实现)` : "")}`,
557
562
  ` 配置加载:${conf.ok ? "正常" : "⚠ " + conf.error}`,
558
563
  ` 解锁剩余:${fmtRemain(remainMs(state.unlockUntil))}(配置写保护豁免)`,
559
564
  ` 放行剩余:${fmtRemain(remainMs(state.bypassUntil))}(全部守卫暂停)`,
@@ -571,7 +576,7 @@ async function executeGuard(ctx, invocation) {
571
576
  const flag = c.disabled ? "(已禁用)" : "";
572
577
  const actions = (c.actions || []).join("/");
573
578
  parts.push(` [${c.ruleId}] ${c.title} ${flag}`);
574
- parts.push(` 等级 ${c.level || "?"}|动作 ${actions}|置信 ${c.confidence}|handler ${c.handler || "generic"}`);
579
+ parts.push(` 等级 ${c.level || "?"}|动作 ${actions}|置信 ${c.confidence}|handler ${c.handler || "(无——规则声明强制但引擎不执行,建议降 D 级或补实现)"}`);
575
580
  }
576
581
  return { kind: "success", text: parts.join("\n") };
577
582
  }
@@ -749,6 +754,27 @@ export function apply(ctx) {
749
754
  "dsh-rule-engine guard"
750
755
  );
751
756
 
757
+ // 1.0 规则 12C B 级留痕:命令文本含回环/内网地址时记录审计,不拦截
758
+ ctx.on("tools/pre-execute", async (exec, next) => {
759
+ try {
760
+ const cmd = exec?.arguments?.command || exec?.arguments?.code || "";
761
+ if (typeof cmd === "string" && cmd.length > 0 && (exec?.name === "pwsh" || exec?.name === "bash") && PRIVATE_NETWORK_RE.test(cmd)) {
762
+ audit({
763
+ kind: "network-flag",
764
+ rule: "12C",
765
+ name: "内网/回环地址访问留痕",
766
+ event: "tools/pre-execute",
767
+ tool: exec?.name,
768
+ reason: `命令文本含回环/内网地址模式:${cmd.slice(0, 160)}`,
769
+ session: sessionIdOfExec(exec)
770
+ });
771
+ }
772
+ } catch {
773
+ // 留痕失败不阻断
774
+ }
775
+ return next();
776
+ });
777
+
752
778
  // 1.1 任务契约 ask 通道:仅在总开关 + askEnabled 时对“应询问”动作返回 ask
753
779
  ctx.on("tools/pre-execute", async (exec, next) => {
754
780
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rule-engine",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "DSH 规则执行引擎 v3:容器解析 AGENTS.md + 理解器 + 匹配机 + 执行框架",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",