cursor-guard 4.8.2 → 4.8.5

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/ROADMAP.md CHANGED
@@ -3,8 +3,8 @@
3
3
  > 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
4
4
  > 每一代向下兼容,低版本功能永远不废弃。
5
5
  >
6
- > **当前版本**:`V4.8.2`
7
- > **文档状态**:`V2` ~ `V4.8.2` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
6
+ > **当前版本**:`V4.8.5`
7
+ > **文档状态**:`V2` ~ `V4.8.5` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
8
8
 
9
9
  ## 阅读导航
10
10
 
@@ -734,6 +734,30 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
734
734
  }
735
735
  ```
736
736
 
737
+ ### V4.8.5:修复变更摘要中 protect 范围外文件误标为"删除"的 Bug ✅
738
+
739
+ | 修复 | 说明 |
740
+ |------|------|
741
+ | **diff-tree 结果过滤 protect 范围** | 当 `protect` 配置非空时,`diff-tree` 对比上一次备份和当前备份的 tree 差异后,额外过滤掉不在 `protect` 范围内的文件。之前这些文件会被误标为"删除"(实际只是不在保护范围内) |
742
+ | **根因** | 用户配置 `protect: ["src/**", "pom.xml"]` 后,`.cursor-guard.json`、`.gitignore`、`.cursor/mcp.json` 等文件不在保护范围内,当前 tree 不含这些文件。`diff-tree` 对比时将其报告为 `D`(删除),但代码只过滤了 `cfg.ignore` 未过滤 `cfg.protect` |
743
+ | **影响** | 变更摘要不再出现"幽灵删除",仅展示 protect 范围内文件的真实变更 |
744
+
745
+ ### V4.8.4:已删除文件恢复命令自动指向父提交 ✅
746
+
747
+ | 修复 | 说明 |
748
+ |------|------|
749
+ | **删除文件恢复命令修正** | 当备份记录中文件 action 为 `D`(删除)时,恢复命令的 `source` 自动改为 `commitHash~1`(父提交,即删除前的版本)。之前指向删除后的 commit,导致恢复报错"文件不存在" |
750
+ | **按钮文案区分** | 删除文件的恢复按钮显示"恢复删除前"(橙色边框),非删除文件显示"复制命令"(默认样式)。同时作用于 Modal 文件详情和 Drawer 文件表格 |
751
+ | **i18n** | 新增 `modal.restorePreDelete` key(EN: "Restore pre-delete" / CN: "恢复删除前") |
752
+
753
+ ### V4.8.3:Doctor MCP 检测修复 + Skill 目录 Junction 补全 ✅
754
+
755
+ | 修复 | 说明 |
756
+ |------|------|
757
+ | **Doctor MCP 检查误报修复** | 之前只检查 `server.js` 文件存在性和 SDK `require.resolve`(esbuild bundle 后必然失败)。新增 `.cursor/mcp.json` 和 `.windsurf/mcp.json` 配置扫描(项目级 + 全局级),如果 `cursor-guard` 已注册则直接 PASS,显示 `registered in .cursor/mcp.json (bundled mode)` |
758
+ | **Skill 目录 junction 补全** | v4.8.2 的 `autoInstallSkill` 有 `if (SKILL.md exists) return` 早退逻辑,导致已安装的旧目录永远不会创建 junction。重构为独立检查:即使 SKILL.md 已存在,仍检测 `references/` 是否为 junction、是否缺少 `mcp/` 运行时目录,不满足则删除旧纯文档目录并重建 junction |
759
+ | **package.json 安装增强** | 优先从根 `package.json` 复制,fallback 到 `guard-version.json` |
760
+
737
761
  ### V4.8.2:Skill 目录运行时完整安装 ✅
738
762
 
739
763
  | 修复 | 说明 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-guard",
3
- "version": "4.8.2",
3
+ "version": "4.8.5",
4
4
  "description": "Protects code from accidental AI overwrite or deletion in Cursor IDE — mandatory pre-write snapshots, review-before-apply, local Git safety net, and deterministic recovery. | 保护代码免受 Cursor AI 代理意外覆写或删除——强制写前快照、预览再执行、本地 Git 安全网、确定性恢复。",
5
5
  "keywords": [
6
6
  "cursor",
@@ -65,6 +65,7 @@ const I18N = {
65
65
  'modal.alertFiles': 'Alert File Details',
66
66
  'modal.col.restore': 'Restore',
67
67
  'modal.copyRestore': 'Copy cmd',
68
+ 'modal.restorePreDelete':'Restore pre-delete',
68
69
  'modal.copied': 'Copied!',
69
70
 
70
71
  'backups.gitCommits': 'Git Commits',
@@ -292,6 +293,7 @@ const I18N = {
292
293
  'modal.alertFiles': '告警文件详情',
293
294
  'modal.col.restore': '恢复',
294
295
  'modal.copyRestore': '复制命令',
296
+ 'modal.restorePreDelete':'恢复删除前',
295
297
  'modal.copied': '已复制!',
296
298
 
297
299
  'backups.gitCommits': 'Git 提交数',
@@ -1306,14 +1308,17 @@ function openFileModal(title, files, projectPath, commitHash) {
1306
1308
  else sorted.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted));
1307
1309
 
1308
1310
  const rows = sorted.map(f => {
1309
- const restoreCmd = commitHash
1310
- ? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${commitHash}" })`
1311
+ const isDeleted = f.action === 'D';
1312
+ const source = isDeleted && commitHash ? `${commitHash}~1` : commitHash;
1313
+ const restoreCmd = source
1314
+ ? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${source}" })`
1311
1315
  : '';
1316
+ const btnLabel = isDeleted ? t('modal.restorePreDelete') : t('modal.copyRestore');
1312
1317
  return `<tr>
1313
1318
  <td class="text-mono modal-file-path" title="${esc(f.path)}">${esc(f.path)}</td>
1314
1319
  <td>${formatFileActionBadge(f.action)}</td>
1315
1320
  <td class="text-mono modal-file-changes">+${f.added || 0} -${f.deleted || 0}</td>
1316
- ${commitHash ? `<td><button class="modal-restore-btn" data-restore-cmd="${esc(restoreCmd)}">${t('modal.copyRestore')}</button></td>` : ''}
1321
+ ${commitHash ? `<td><button class="modal-restore-btn${isDeleted ? ' btn-deleted' : ''}" data-restore-cmd="${esc(restoreCmd)}">${btnLabel}</button></td>` : ''}
1317
1322
  </tr>`;
1318
1323
  }).join('');
1319
1324
 
@@ -1339,9 +1344,10 @@ function openFileModal(title, files, projectPath, commitHash) {
1339
1344
  const btn = e.target.closest('[data-restore-cmd]');
1340
1345
  if (btn) {
1341
1346
  copyText(btn.dataset.restoreCmd);
1347
+ const origLabel = btn.classList.contains('btn-deleted') ? t('modal.restorePreDelete') : t('modal.copyRestore');
1342
1348
  btn.textContent = t('modal.copied');
1343
1349
  btn.classList.add('copied');
1344
- setTimeout(() => { btn.textContent = t('modal.copyRestore'); btn.classList.remove('copied'); }, 1500);
1350
+ setTimeout(() => { btn.textContent = origLabel; btn.classList.remove('copied'); }, 1500);
1345
1351
  }
1346
1352
  });
1347
1353
 
@@ -1378,12 +1384,15 @@ function renderDrawerFilesTable(files, sortKey, commitHash, projectPath) {
1378
1384
  else sorted.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted));
1379
1385
  const hasRestore = !!commitHash;
1380
1386
  const rows = sorted.map(f => {
1381
- const cmd = hasRestore ? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${commitHash}" })` : '';
1387
+ const isDeleted = f.action === 'D';
1388
+ const source = isDeleted && commitHash ? `${commitHash}~1` : commitHash;
1389
+ const cmd = hasRestore ? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${source}" })` : '';
1390
+ const btnLabel = isDeleted ? t('modal.restorePreDelete') : t('modal.copyRestore');
1382
1391
  return `<tr>
1383
1392
  <td class="text-mono drawer-file-path">${esc(f.path)}</td>
1384
1393
  <td>${formatFileActionBadge(f.action)}</td>
1385
1394
  <td class="text-mono drawer-file-changes">+${f.added} -${f.deleted}</td>
1386
- ${hasRestore ? `<td><button class="modal-restore-btn" data-restore-cmd="${esc(cmd)}">${t('modal.copyRestore')}</button></td>` : ''}
1395
+ ${hasRestore ? `<td><button class="modal-restore-btn${isDeleted ? ' btn-deleted' : ''}" data-restore-cmd="${esc(cmd)}">${btnLabel}</button></td>` : ''}
1387
1396
  </tr>`;
1388
1397
  }).join('');
1389
1398
  return `<table class="drawer-files-table">
@@ -1490,9 +1499,10 @@ function openRestoreDrawer(backup) {
1490
1499
  const restoreBtn = e.target.closest('[data-restore-cmd]');
1491
1500
  if (restoreBtn) {
1492
1501
  copyText(restoreBtn.dataset.restoreCmd);
1502
+ const origLabel = restoreBtn.classList.contains('btn-deleted') ? t('modal.restorePreDelete') : t('modal.copyRestore');
1493
1503
  restoreBtn.textContent = t('modal.copied');
1494
1504
  restoreBtn.classList.add('copied');
1495
- setTimeout(() => { restoreBtn.textContent = t('modal.copyRestore'); restoreBtn.classList.remove('copied'); }, 1500);
1505
+ setTimeout(() => { restoreBtn.textContent = origLabel; restoreBtn.classList.remove('copied'); }, 1500);
1496
1506
  }
1497
1507
  });
1498
1508
  };
@@ -1443,6 +1443,15 @@ main {
1443
1443
  color: #fff;
1444
1444
  border-color: var(--green);
1445
1445
  }
1446
+ .modal-restore-btn.btn-deleted {
1447
+ border-color: var(--orange, #f0ad4e);
1448
+ color: var(--orange, #f0ad4e);
1449
+ }
1450
+ .modal-restore-btn.btn-deleted:hover {
1451
+ background: var(--orange, #f0ad4e);
1452
+ color: #fff;
1453
+ border-color: var(--orange, #f0ad4e);
1454
+ }
1446
1455
 
1447
1456
  /* ── Responsive ───────────────────────────────────────────── */
1448
1457
 
@@ -253,49 +253,69 @@ function runDiagnostics(projectDir) {
253
253
  }
254
254
 
255
255
  // 14. MCP server status
256
+ // Strategy: check multiple sources — mcp.json config, local server.js, SDK availability
256
257
  const mcpServerPath = path.resolve(__dirname, '../../mcp/server.js');
257
258
  const mcpServerExists = fs.existsSync(mcpServerPath);
258
259
 
260
+ // Check if cursor-guard is registered in any mcp.json (project or global)
261
+ let mcpConfigured = false;
262
+ let mcpConfigSource = '';
263
+ const home = process.env.USERPROFILE || process.env.HOME || '';
264
+ const mcpJsonCandidates = [
265
+ projectDir ? path.join(projectDir, '.cursor', 'mcp.json') : null,
266
+ projectDir ? path.join(projectDir, '.windsurf', 'mcp.json') : null,
267
+ path.join(home, '.cursor', 'mcp.json'),
268
+ path.join(home, '.windsurf', 'mcp.json'),
269
+ ].filter(Boolean);
270
+ for (const mcpJsonPath of mcpJsonCandidates) {
271
+ try {
272
+ if (!fs.existsSync(mcpJsonPath)) continue;
273
+ const mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
274
+ if (mcpJson.mcpServers?.['cursor-guard']) {
275
+ mcpConfigured = true;
276
+ mcpConfigSource = path.relative(projectDir || home, mcpJsonPath);
277
+ break;
278
+ }
279
+ } catch { /* ignore */ }
280
+ }
281
+
282
+ // Check SDK availability (for non-bundled installations)
259
283
  let mcpSdkAvailable = false;
260
284
  let mcpSdkVersion = null;
261
285
  const skillRoot = path.resolve(__dirname, '../../..');
262
- // Search multiple candidate locations for SDK package.json
263
286
  const sdkCandidates = [
264
287
  path.join(skillRoot, 'node_modules', '@modelcontextprotocol', 'sdk', 'package.json'),
265
288
  ];
266
289
  for (const candidate of sdkCandidates) {
267
290
  try {
268
- const resolved = path.resolve(candidate);
269
- if (fs.existsSync(resolved)) {
270
- const mcpPkg = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
291
+ if (fs.existsSync(candidate)) {
292
+ mcpSdkVersion = JSON.parse(fs.readFileSync(candidate, 'utf-8')).version;
271
293
  mcpSdkAvailable = true;
272
- mcpSdkVersion = mcpPkg.version;
273
294
  break;
274
295
  }
275
296
  } catch { /* ignore */ }
276
297
  }
277
298
  if (!mcpSdkAvailable) {
278
- // Fallback: try require.resolve from Node's module paths.
279
- // Some SDK versions restrict subpath access via exports, so try
280
- // the main entry first and derive the package.json from it.
281
299
  try {
282
300
  const mainPath = require.resolve('@modelcontextprotocol/sdk');
283
301
  const sdkDir = mainPath.replace(/[/\\]dist[/\\].*$/, '').replace(/[/\\]src[/\\].*$/, '');
284
302
  const pkgPath = path.join(sdkDir, 'package.json');
285
303
  if (fs.existsSync(pkgPath)) {
286
- const mcpPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
304
+ mcpSdkVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
287
305
  mcpSdkAvailable = true;
288
- mcpSdkVersion = mcpPkg.version;
289
306
  }
290
307
  } catch { /* not installed */ }
291
308
  }
292
309
 
293
- if (mcpServerExists && mcpSdkAvailable) {
310
+ if (mcpConfigured) {
311
+ const detail = mcpSdkAvailable
312
+ ? `registered in ${mcpConfigSource}, SDK ${mcpSdkVersion}`
313
+ : `registered in ${mcpConfigSource} (bundled mode)`;
314
+ check('MCP server', 'PASS', detail);
315
+ } else if (mcpServerExists && mcpSdkAvailable) {
294
316
  check('MCP server', 'PASS', `server.js found, SDK ${mcpSdkVersion}`);
295
317
  } else if (mcpServerExists && !mcpSdkAvailable) {
296
318
  check('MCP server', 'WARN', 'server.js found but @modelcontextprotocol/sdk not installed — run: cd <skill-dir> && npm install');
297
- } else if (!mcpServerExists && mcpSdkAvailable) {
298
- check('MCP server', 'WARN', `SDK installed (${mcpSdkVersion}) but server.js not found at expected path`);
299
319
  } else {
300
320
  check('MCP server', 'WARN', 'MCP not configured (optional — cursor-guard works without it)');
301
321
  }
@@ -156,6 +156,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
156
156
  : 'M';
157
157
  const fileName = filePart.split('\t').pop();
158
158
  if (matchesAny(cfg.ignore, fileName) || matchesAny(cfg.ignore, path.basename(fileName))) continue;
159
+ if (cfg.protect.length > 0 && !matchesAny(cfg.protect, fileName, { strict: true })) continue;
159
160
  groups[key].push(fileName);
160
161
  }
161
162
  changedCount = Object.values(groups).reduce((sum, arr) => sum + arr.length, 0);
@@ -199,7 +200,8 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
199
200
  const lsInitial = git(['ls-tree', '--name-only', '-r', newTree], { cwd, allowFail: true });
200
201
  if (lsInitial) {
201
202
  const files = lsInitial.split('\n').filter(Boolean)
202
- .filter(f => !matchesAny(cfg.ignore, f) && !matchesAny(cfg.ignore, path.basename(f)));
203
+ .filter(f => !matchesAny(cfg.ignore, f) && !matchesAny(cfg.ignore, path.basename(f)))
204
+ .filter(f => cfg.protect.length === 0 || matchesAny(cfg.protect, f, { strict: true }));
203
205
  changedCount = files.length;
204
206
  const sample = files.slice(0, 5).join(', ');
205
207
  incrementalSummary = `Added ${files.length}: ${sample}${files.length > 5 ? ', ...' : ''}`;
@@ -65,6 +65,7 @@ const I18N = {
65
65
  'modal.alertFiles': 'Alert File Details',
66
66
  'modal.col.restore': 'Restore',
67
67
  'modal.copyRestore': 'Copy cmd',
68
+ 'modal.restorePreDelete':'Restore pre-delete',
68
69
  'modal.copied': 'Copied!',
69
70
 
70
71
  'backups.gitCommits': 'Git Commits',
@@ -292,6 +293,7 @@ const I18N = {
292
293
  'modal.alertFiles': '告警文件详情',
293
294
  'modal.col.restore': '恢复',
294
295
  'modal.copyRestore': '复制命令',
296
+ 'modal.restorePreDelete':'恢复删除前',
295
297
  'modal.copied': '已复制!',
296
298
 
297
299
  'backups.gitCommits': 'Git 提交数',
@@ -1306,14 +1308,17 @@ function openFileModal(title, files, projectPath, commitHash) {
1306
1308
  else sorted.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted));
1307
1309
 
1308
1310
  const rows = sorted.map(f => {
1309
- const restoreCmd = commitHash
1310
- ? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${commitHash}" })`
1311
+ const isDeleted = f.action === 'D';
1312
+ const source = isDeleted && commitHash ? `${commitHash}~1` : commitHash;
1313
+ const restoreCmd = source
1314
+ ? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${source}" })`
1311
1315
  : '';
1316
+ const btnLabel = isDeleted ? t('modal.restorePreDelete') : t('modal.copyRestore');
1312
1317
  return `<tr>
1313
1318
  <td class="text-mono modal-file-path" title="${esc(f.path)}">${esc(f.path)}</td>
1314
1319
  <td>${formatFileActionBadge(f.action)}</td>
1315
1320
  <td class="text-mono modal-file-changes">+${f.added || 0} -${f.deleted || 0}</td>
1316
- ${commitHash ? `<td><button class="modal-restore-btn" data-restore-cmd="${esc(restoreCmd)}">${t('modal.copyRestore')}</button></td>` : ''}
1321
+ ${commitHash ? `<td><button class="modal-restore-btn${isDeleted ? ' btn-deleted' : ''}" data-restore-cmd="${esc(restoreCmd)}">${btnLabel}</button></td>` : ''}
1317
1322
  </tr>`;
1318
1323
  }).join('');
1319
1324
 
@@ -1339,9 +1344,10 @@ function openFileModal(title, files, projectPath, commitHash) {
1339
1344
  const btn = e.target.closest('[data-restore-cmd]');
1340
1345
  if (btn) {
1341
1346
  copyText(btn.dataset.restoreCmd);
1347
+ const origLabel = btn.classList.contains('btn-deleted') ? t('modal.restorePreDelete') : t('modal.copyRestore');
1342
1348
  btn.textContent = t('modal.copied');
1343
1349
  btn.classList.add('copied');
1344
- setTimeout(() => { btn.textContent = t('modal.copyRestore'); btn.classList.remove('copied'); }, 1500);
1350
+ setTimeout(() => { btn.textContent = origLabel; btn.classList.remove('copied'); }, 1500);
1345
1351
  }
1346
1352
  });
1347
1353
 
@@ -1378,12 +1384,15 @@ function renderDrawerFilesTable(files, sortKey, commitHash, projectPath) {
1378
1384
  else sorted.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted));
1379
1385
  const hasRestore = !!commitHash;
1380
1386
  const rows = sorted.map(f => {
1381
- const cmd = hasRestore ? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${commitHash}" })` : '';
1387
+ const isDeleted = f.action === 'D';
1388
+ const source = isDeleted && commitHash ? `${commitHash}~1` : commitHash;
1389
+ const cmd = hasRestore ? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${source}" })` : '';
1390
+ const btnLabel = isDeleted ? t('modal.restorePreDelete') : t('modal.copyRestore');
1382
1391
  return `<tr>
1383
1392
  <td class="text-mono drawer-file-path">${esc(f.path)}</td>
1384
1393
  <td>${formatFileActionBadge(f.action)}</td>
1385
1394
  <td class="text-mono drawer-file-changes">+${f.added} -${f.deleted}</td>
1386
- ${hasRestore ? `<td><button class="modal-restore-btn" data-restore-cmd="${esc(cmd)}">${t('modal.copyRestore')}</button></td>` : ''}
1395
+ ${hasRestore ? `<td><button class="modal-restore-btn${isDeleted ? ' btn-deleted' : ''}" data-restore-cmd="${esc(cmd)}">${btnLabel}</button></td>` : ''}
1387
1396
  </tr>`;
1388
1397
  }).join('');
1389
1398
  return `<table class="drawer-files-table">
@@ -1490,9 +1499,10 @@ function openRestoreDrawer(backup) {
1490
1499
  const restoreBtn = e.target.closest('[data-restore-cmd]');
1491
1500
  if (restoreBtn) {
1492
1501
  copyText(restoreBtn.dataset.restoreCmd);
1502
+ const origLabel = restoreBtn.classList.contains('btn-deleted') ? t('modal.restorePreDelete') : t('modal.copyRestore');
1493
1503
  restoreBtn.textContent = t('modal.copied');
1494
1504
  restoreBtn.classList.add('copied');
1495
- setTimeout(() => { restoreBtn.textContent = t('modal.copyRestore'); restoreBtn.classList.remove('copied'); }, 1500);
1505
+ setTimeout(() => { restoreBtn.textContent = origLabel; restoreBtn.classList.remove('copied'); }, 1500);
1496
1506
  }
1497
1507
  });
1498
1508
  };
@@ -1443,6 +1443,15 @@ main {
1443
1443
  color: #fff;
1444
1444
  border-color: var(--green);
1445
1445
  }
1446
+ .modal-restore-btn.btn-deleted {
1447
+ border-color: var(--orange, #f0ad4e);
1448
+ color: var(--orange, #f0ad4e);
1449
+ }
1450
+ .modal-restore-btn.btn-deleted:hover {
1451
+ background: var(--orange, #f0ad4e);
1452
+ color: #fff;
1453
+ border-color: var(--orange, #f0ad4e);
1454
+ }
1446
1455
 
1447
1456
  /* ── Responsive ───────────────────────────────────────────── */
1448
1457
 
@@ -1 +1 @@
1
- {"version":"4.8.2"}
1
+ {"version":"4.8.5"}
@@ -67,47 +67,64 @@ function autoInstallSkill(extRoot, homePath, dirName) {
67
67
  if (!skillSrc) return actions;
68
68
 
69
69
  const skillTarget = path.join(homePath, 'skills', 'cursor-guard');
70
- const skillMdTarget = path.join(skillTarget, 'SKILL.md');
71
-
72
- if (fs.existsSync(skillMdTarget)) return actions;
73
-
74
70
  fs.mkdirSync(skillTarget, { recursive: true });
75
71
 
72
+ // ── Install/update SKILL.md and ROADMAP.md ──
76
73
  const skillMdSrc = path.join(skillSrc, 'SKILL.md');
77
- if (fs.existsSync(skillMdSrc)) {
74
+ const skillMdTarget = path.join(skillTarget, 'SKILL.md');
75
+ if (fs.existsSync(skillMdSrc) && !fs.existsSync(skillMdTarget)) {
78
76
  fs.copyFileSync(skillMdSrc, skillMdTarget);
79
77
  actions.push('SKILL.md installed');
80
78
  }
81
79
 
82
80
  const roadmapSrc = path.join(skillSrc, 'ROADMAP.md');
83
- if (fs.existsSync(roadmapSrc)) {
84
- fs.copyFileSync(roadmapSrc, path.join(skillTarget, 'ROADMAP.md'));
81
+ const roadmapDst = path.join(skillTarget, 'ROADMAP.md');
82
+ if (fs.existsSync(roadmapSrc) && !fs.existsSync(roadmapDst)) {
83
+ fs.copyFileSync(roadmapSrc, roadmapDst);
85
84
  }
86
85
 
87
- // Link references/ extension directory so SKILL.md paths resolve correctly
88
- // (mcp/server.js, lib/core/*, dashboard/, bin/ etc.)
86
+ // ── Ensure references/ junction exists (runs even for existing installations) ──
89
87
  const refsTarget = path.join(skillTarget, 'references');
90
- if (!fs.existsSync(refsTarget)) {
88
+ const refsIsJunction = _isSymlinkOrJunction(refsTarget);
89
+ const refsIsPlainDir = !refsIsJunction && fs.existsSync(refsTarget);
90
+ const refsMissingRuntime = refsIsPlainDir && !fs.existsSync(path.join(refsTarget, 'mcp'));
91
+
92
+ if (!fs.existsSync(refsTarget) || refsMissingRuntime) {
93
+ // Remove old plain directory if it only has docs (no runtime)
94
+ if (refsMissingRuntime) {
95
+ try { fs.rmSync(refsTarget, { recursive: true, force: true }); } catch { /* ok */ }
96
+ }
91
97
  try {
92
98
  fs.symlinkSync(extRoot, refsTarget, 'junction');
93
99
  actions.push('references/ linked');
94
100
  } catch {
95
- // junction failed (rare) — fall back to copying essential docs only
96
101
  fs.mkdirSync(refsTarget, { recursive: true });
97
102
  _copyDocFiles(skillSrc, refsTarget);
98
103
  }
99
104
  }
100
105
 
101
- // Copy package.json so `require('../../package.json')` in source mode works
102
- const pkgSrc = path.join(extRoot, '..', '..', 'package.json');
106
+ // ── Ensure package.json exists ──
103
107
  const pkgDst = path.join(skillTarget, 'package.json');
104
- if (fs.existsSync(pkgSrc) && !fs.existsSync(pkgDst)) {
105
- fs.copyFileSync(pkgSrc, pkgDst);
108
+ if (!fs.existsSync(pkgDst)) {
109
+ const pkgSrc = path.join(extRoot, '..', '..', 'package.json');
110
+ const guardVer = path.join(extRoot, 'guard-version.json');
111
+ if (fs.existsSync(pkgSrc)) {
112
+ fs.copyFileSync(pkgSrc, pkgDst);
113
+ } else if (fs.existsSync(guardVer)) {
114
+ fs.copyFileSync(guardVer, pkgDst);
115
+ }
106
116
  }
107
117
 
108
118
  return actions;
109
119
  }
110
120
 
121
+ function _isSymlinkOrJunction(p) {
122
+ try {
123
+ const stat = fs.lstatSync(p);
124
+ return stat.isSymbolicLink();
125
+ } catch { return false; }
126
+ }
127
+
111
128
  function _copyDocFiles(skillSrc, refsTarget) {
112
129
  const docs = [
113
130
  'config-reference.md', 'config-reference.zh-CN.md',
@@ -253,49 +253,69 @@ function runDiagnostics(projectDir) {
253
253
  }
254
254
 
255
255
  // 14. MCP server status
256
+ // Strategy: check multiple sources — mcp.json config, local server.js, SDK availability
256
257
  const mcpServerPath = path.resolve(__dirname, '../../mcp/server.js');
257
258
  const mcpServerExists = fs.existsSync(mcpServerPath);
258
259
 
260
+ // Check if cursor-guard is registered in any mcp.json (project or global)
261
+ let mcpConfigured = false;
262
+ let mcpConfigSource = '';
263
+ const home = process.env.USERPROFILE || process.env.HOME || '';
264
+ const mcpJsonCandidates = [
265
+ projectDir ? path.join(projectDir, '.cursor', 'mcp.json') : null,
266
+ projectDir ? path.join(projectDir, '.windsurf', 'mcp.json') : null,
267
+ path.join(home, '.cursor', 'mcp.json'),
268
+ path.join(home, '.windsurf', 'mcp.json'),
269
+ ].filter(Boolean);
270
+ for (const mcpJsonPath of mcpJsonCandidates) {
271
+ try {
272
+ if (!fs.existsSync(mcpJsonPath)) continue;
273
+ const mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
274
+ if (mcpJson.mcpServers?.['cursor-guard']) {
275
+ mcpConfigured = true;
276
+ mcpConfigSource = path.relative(projectDir || home, mcpJsonPath);
277
+ break;
278
+ }
279
+ } catch { /* ignore */ }
280
+ }
281
+
282
+ // Check SDK availability (for non-bundled installations)
259
283
  let mcpSdkAvailable = false;
260
284
  let mcpSdkVersion = null;
261
285
  const skillRoot = path.resolve(__dirname, '../../..');
262
- // Search multiple candidate locations for SDK package.json
263
286
  const sdkCandidates = [
264
287
  path.join(skillRoot, 'node_modules', '@modelcontextprotocol', 'sdk', 'package.json'),
265
288
  ];
266
289
  for (const candidate of sdkCandidates) {
267
290
  try {
268
- const resolved = path.resolve(candidate);
269
- if (fs.existsSync(resolved)) {
270
- const mcpPkg = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
291
+ if (fs.existsSync(candidate)) {
292
+ mcpSdkVersion = JSON.parse(fs.readFileSync(candidate, 'utf-8')).version;
271
293
  mcpSdkAvailable = true;
272
- mcpSdkVersion = mcpPkg.version;
273
294
  break;
274
295
  }
275
296
  } catch { /* ignore */ }
276
297
  }
277
298
  if (!mcpSdkAvailable) {
278
- // Fallback: try require.resolve from Node's module paths.
279
- // Some SDK versions restrict subpath access via exports, so try
280
- // the main entry first and derive the package.json from it.
281
299
  try {
282
300
  const mainPath = require.resolve('@modelcontextprotocol/sdk');
283
301
  const sdkDir = mainPath.replace(/[/\\]dist[/\\].*$/, '').replace(/[/\\]src[/\\].*$/, '');
284
302
  const pkgPath = path.join(sdkDir, 'package.json');
285
303
  if (fs.existsSync(pkgPath)) {
286
- const mcpPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
304
+ mcpSdkVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
287
305
  mcpSdkAvailable = true;
288
- mcpSdkVersion = mcpPkg.version;
289
306
  }
290
307
  } catch { /* not installed */ }
291
308
  }
292
309
 
293
- if (mcpServerExists && mcpSdkAvailable) {
310
+ if (mcpConfigured) {
311
+ const detail = mcpSdkAvailable
312
+ ? `registered in ${mcpConfigSource}, SDK ${mcpSdkVersion}`
313
+ : `registered in ${mcpConfigSource} (bundled mode)`;
314
+ check('MCP server', 'PASS', detail);
315
+ } else if (mcpServerExists && mcpSdkAvailable) {
294
316
  check('MCP server', 'PASS', `server.js found, SDK ${mcpSdkVersion}`);
295
317
  } else if (mcpServerExists && !mcpSdkAvailable) {
296
318
  check('MCP server', 'WARN', 'server.js found but @modelcontextprotocol/sdk not installed — run: cd <skill-dir> && npm install');
297
- } else if (!mcpServerExists && mcpSdkAvailable) {
298
- check('MCP server', 'WARN', `SDK installed (${mcpSdkVersion}) but server.js not found at expected path`);
299
319
  } else {
300
320
  check('MCP server', 'WARN', 'MCP not configured (optional — cursor-guard works without it)');
301
321
  }
@@ -156,6 +156,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
156
156
  : 'M';
157
157
  const fileName = filePart.split('\t').pop();
158
158
  if (matchesAny(cfg.ignore, fileName) || matchesAny(cfg.ignore, path.basename(fileName))) continue;
159
+ if (cfg.protect.length > 0 && !matchesAny(cfg.protect, fileName, { strict: true })) continue;
159
160
  groups[key].push(fileName);
160
161
  }
161
162
  changedCount = Object.values(groups).reduce((sum, arr) => sum + arr.length, 0);
@@ -199,7 +200,8 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
199
200
  const lsInitial = git(['ls-tree', '--name-only', '-r', newTree], { cwd, allowFail: true });
200
201
  if (lsInitial) {
201
202
  const files = lsInitial.split('\n').filter(Boolean)
202
- .filter(f => !matchesAny(cfg.ignore, f) && !matchesAny(cfg.ignore, path.basename(f)));
203
+ .filter(f => !matchesAny(cfg.ignore, f) && !matchesAny(cfg.ignore, path.basename(f)))
204
+ .filter(f => cfg.protect.length === 0 || matchesAny(cfg.protect, f, { strict: true }));
203
205
  changedCount = files.length;
204
206
  const sample = files.slice(0, 5).join(', ');
205
207
  incrementalSummary = `Added ${files.length}: ${sample}${files.length > 5 ? ', ...' : ''}`;
@@ -35568,7 +35568,7 @@ var require_package = __commonJS({
35568
35568
  "package.json"(exports2, module2) {
35569
35569
  module2.exports = {
35570
35570
  name: "cursor-guard",
35571
- version: "4.8.2",
35571
+ version: "4.8.5",
35572
35572
  description: "Protects code from accidental AI overwrite or deletion in Cursor IDE \u2014 mandatory pre-write snapshots, review-before-apply, local Git safety net, and deterministic recovery. | \u4FDD\u62A4\u4EE3\u7801\u514D\u53D7 Cursor AI \u4EE3\u7406\u610F\u5916\u8986\u5199\u6216\u5220\u9664\u2014\u2014\u5F3A\u5236\u5199\u524D\u5FEB\u7167\u3001\u9884\u89C8\u518D\u6267\u884C\u3001\u672C\u5730 Git \u5B89\u5168\u7F51\u3001\u786E\u5B9A\u6027\u6062\u590D\u3002",
35573
35573
  keywords: [
35574
35574
  "cursor",
@@ -35875,6 +35875,27 @@ var require_doctor = __commonJS({
35875
35875
  }
35876
35876
  const mcpServerPath = path2.resolve(__dirname, "../../mcp/server.js");
35877
35877
  const mcpServerExists = fs.existsSync(mcpServerPath);
35878
+ let mcpConfigured = false;
35879
+ let mcpConfigSource = "";
35880
+ const home = process.env.USERPROFILE || process.env.HOME || "";
35881
+ const mcpJsonCandidates = [
35882
+ projectDir ? path2.join(projectDir, ".cursor", "mcp.json") : null,
35883
+ projectDir ? path2.join(projectDir, ".windsurf", "mcp.json") : null,
35884
+ path2.join(home, ".cursor", "mcp.json"),
35885
+ path2.join(home, ".windsurf", "mcp.json")
35886
+ ].filter(Boolean);
35887
+ for (const mcpJsonPath of mcpJsonCandidates) {
35888
+ try {
35889
+ if (!fs.existsSync(mcpJsonPath)) continue;
35890
+ const mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
35891
+ if (mcpJson.mcpServers?.["cursor-guard"]) {
35892
+ mcpConfigured = true;
35893
+ mcpConfigSource = path2.relative(projectDir || home, mcpJsonPath);
35894
+ break;
35895
+ }
35896
+ } catch {
35897
+ }
35898
+ }
35878
35899
  let mcpSdkAvailable = false;
35879
35900
  let mcpSdkVersion = null;
35880
35901
  const skillRoot = path2.resolve(__dirname, "../../..");
@@ -35883,11 +35904,9 @@ var require_doctor = __commonJS({
35883
35904
  ];
35884
35905
  for (const candidate of sdkCandidates) {
35885
35906
  try {
35886
- const resolved = path2.resolve(candidate);
35887
- if (fs.existsSync(resolved)) {
35888
- const mcpPkg = JSON.parse(fs.readFileSync(resolved, "utf-8"));
35907
+ if (fs.existsSync(candidate)) {
35908
+ mcpSdkVersion = JSON.parse(fs.readFileSync(candidate, "utf-8")).version;
35889
35909
  mcpSdkAvailable = true;
35890
- mcpSdkVersion = mcpPkg.version;
35891
35910
  break;
35892
35911
  }
35893
35912
  } catch {
@@ -35899,19 +35918,19 @@ var require_doctor = __commonJS({
35899
35918
  const sdkDir = mainPath.replace(/[/\\]dist[/\\].*$/, "").replace(/[/\\]src[/\\].*$/, "");
35900
35919
  const pkgPath = path2.join(sdkDir, "package.json");
35901
35920
  if (fs.existsSync(pkgPath)) {
35902
- const mcpPkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
35921
+ mcpSdkVersion = JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
35903
35922
  mcpSdkAvailable = true;
35904
- mcpSdkVersion = mcpPkg.version;
35905
35923
  }
35906
35924
  } catch {
35907
35925
  }
35908
35926
  }
35909
- if (mcpServerExists && mcpSdkAvailable) {
35927
+ if (mcpConfigured) {
35928
+ const detail = mcpSdkAvailable ? `registered in ${mcpConfigSource}, SDK ${mcpSdkVersion}` : `registered in ${mcpConfigSource} (bundled mode)`;
35929
+ check("MCP server", "PASS", detail);
35930
+ } else if (mcpServerExists && mcpSdkAvailable) {
35910
35931
  check("MCP server", "PASS", `server.js found, SDK ${mcpSdkVersion}`);
35911
35932
  } else if (mcpServerExists && !mcpSdkAvailable) {
35912
35933
  check("MCP server", "WARN", "server.js found but @modelcontextprotocol/sdk not installed \u2014 run: cd <skill-dir> && npm install");
35913
- } else if (!mcpServerExists && mcpSdkAvailable) {
35914
- check("MCP server", "WARN", `SDK installed (${mcpSdkVersion}) but server.js not found at expected path`);
35915
35934
  } else {
35916
35935
  check("MCP server", "WARN", "MCP not configured (optional \u2014 cursor-guard works without it)");
35917
35936
  }
@@ -36077,6 +36096,7 @@ var require_snapshot = __commonJS({
36077
36096
  const key = code.startsWith("R") ? "R" : code === "D" ? "D" : code === "A" ? "A" : "M";
36078
36097
  const fileName = filePart.split(" ").pop();
36079
36098
  if (matchesAny(cfg.ignore, fileName) || matchesAny(cfg.ignore, path2.basename(fileName))) continue;
36099
+ if (cfg.protect.length > 0 && !matchesAny(cfg.protect, fileName, { strict: true })) continue;
36080
36100
  groups[key].push(fileName);
36081
36101
  }
36082
36102
  changedCount = Object.values(groups).reduce((sum, arr) => sum + arr.length, 0);
@@ -36108,7 +36128,7 @@ var require_snapshot = __commonJS({
36108
36128
  } else {
36109
36129
  const lsInitial = git(["ls-tree", "--name-only", "-r", newTree], { cwd, allowFail: true });
36110
36130
  if (lsInitial) {
36111
- const files = lsInitial.split("\n").filter(Boolean).filter((f) => !matchesAny(cfg.ignore, f) && !matchesAny(cfg.ignore, path2.basename(f)));
36131
+ const files = lsInitial.split("\n").filter(Boolean).filter((f) => !matchesAny(cfg.ignore, f) && !matchesAny(cfg.ignore, path2.basename(f))).filter((f) => cfg.protect.length === 0 || matchesAny(cfg.protect, f, { strict: true }));
36112
36132
  changedCount = files.length;
36113
36133
  const sample = files.slice(0, 5).join(", ");
36114
36134
  incrementalSummary = `Added ${files.length}: ${sample}${files.length > 5 ? ", ..." : ""}`;
@@ -2,7 +2,7 @@
2
2
  "name": "cursor-guard-ide",
3
3
  "displayName": "Cursor Guard",
4
4
  "description": "AI code protection dashboard embedded in your IDE — real-time alerts, backup history, one-click snapshots",
5
- "version": "4.8.2",
5
+ "version": "4.8.5",
6
6
  "publisher": "zhangqiang8vipp",
7
7
  "license": "BUSL-1.1",
8
8
  "engines": {
@@ -3,8 +3,8 @@
3
3
  > 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
4
4
  > 每一代向下兼容,低版本功能永远不废弃。
5
5
  >
6
- > **当前版本**:`V4.8.2`
7
- > **文档状态**:`V2` ~ `V4.8.2` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
6
+ > **当前版本**:`V4.8.5`
7
+ > **文档状态**:`V2` ~ `V4.8.5` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
8
8
 
9
9
  ## 阅读导航
10
10
 
@@ -734,6 +734,30 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
734
734
  }
735
735
  ```
736
736
 
737
+ ### V4.8.5:修复变更摘要中 protect 范围外文件误标为"删除"的 Bug ✅
738
+
739
+ | 修复 | 说明 |
740
+ |------|------|
741
+ | **diff-tree 结果过滤 protect 范围** | 当 `protect` 配置非空时,`diff-tree` 对比上一次备份和当前备份的 tree 差异后,额外过滤掉不在 `protect` 范围内的文件。之前这些文件会被误标为"删除"(实际只是不在保护范围内) |
742
+ | **根因** | 用户配置 `protect: ["src/**", "pom.xml"]` 后,`.cursor-guard.json`、`.gitignore`、`.cursor/mcp.json` 等文件不在保护范围内,当前 tree 不含这些文件。`diff-tree` 对比时将其报告为 `D`(删除),但代码只过滤了 `cfg.ignore` 未过滤 `cfg.protect` |
743
+ | **影响** | 变更摘要不再出现"幽灵删除",仅展示 protect 范围内文件的真实变更 |
744
+
745
+ ### V4.8.4:已删除文件恢复命令自动指向父提交 ✅
746
+
747
+ | 修复 | 说明 |
748
+ |------|------|
749
+ | **删除文件恢复命令修正** | 当备份记录中文件 action 为 `D`(删除)时,恢复命令的 `source` 自动改为 `commitHash~1`(父提交,即删除前的版本)。之前指向删除后的 commit,导致恢复报错"文件不存在" |
750
+ | **按钮文案区分** | 删除文件的恢复按钮显示"恢复删除前"(橙色边框),非删除文件显示"复制命令"(默认样式)。同时作用于 Modal 文件详情和 Drawer 文件表格 |
751
+ | **i18n** | 新增 `modal.restorePreDelete` key(EN: "Restore pre-delete" / CN: "恢复删除前") |
752
+
753
+ ### V4.8.3:Doctor MCP 检测修复 + Skill 目录 Junction 补全 ✅
754
+
755
+ | 修复 | 说明 |
756
+ |------|------|
757
+ | **Doctor MCP 检查误报修复** | 之前只检查 `server.js` 文件存在性和 SDK `require.resolve`(esbuild bundle 后必然失败)。新增 `.cursor/mcp.json` 和 `.windsurf/mcp.json` 配置扫描(项目级 + 全局级),如果 `cursor-guard` 已注册则直接 PASS,显示 `registered in .cursor/mcp.json (bundled mode)` |
758
+ | **Skill 目录 junction 补全** | v4.8.2 的 `autoInstallSkill` 有 `if (SKILL.md exists) return` 早退逻辑,导致已安装的旧目录永远不会创建 junction。重构为独立检查:即使 SKILL.md 已存在,仍检测 `references/` 是否为 junction、是否缺少 `mcp/` 运行时目录,不满足则删除旧纯文档目录并重建 junction |
759
+ | **package.json 安装增强** | 优先从根 `package.json` 复制,fallback 到 `guard-version.json` |
760
+
737
761
  ### V4.8.2:Skill 目录运行时完整安装 ✅
738
762
 
739
763
  | 修复 | 说明 |
@@ -67,47 +67,64 @@ function autoInstallSkill(extRoot, homePath, dirName) {
67
67
  if (!skillSrc) return actions;
68
68
 
69
69
  const skillTarget = path.join(homePath, 'skills', 'cursor-guard');
70
- const skillMdTarget = path.join(skillTarget, 'SKILL.md');
71
-
72
- if (fs.existsSync(skillMdTarget)) return actions;
73
-
74
70
  fs.mkdirSync(skillTarget, { recursive: true });
75
71
 
72
+ // ── Install/update SKILL.md and ROADMAP.md ──
76
73
  const skillMdSrc = path.join(skillSrc, 'SKILL.md');
77
- if (fs.existsSync(skillMdSrc)) {
74
+ const skillMdTarget = path.join(skillTarget, 'SKILL.md');
75
+ if (fs.existsSync(skillMdSrc) && !fs.existsSync(skillMdTarget)) {
78
76
  fs.copyFileSync(skillMdSrc, skillMdTarget);
79
77
  actions.push('SKILL.md installed');
80
78
  }
81
79
 
82
80
  const roadmapSrc = path.join(skillSrc, 'ROADMAP.md');
83
- if (fs.existsSync(roadmapSrc)) {
84
- fs.copyFileSync(roadmapSrc, path.join(skillTarget, 'ROADMAP.md'));
81
+ const roadmapDst = path.join(skillTarget, 'ROADMAP.md');
82
+ if (fs.existsSync(roadmapSrc) && !fs.existsSync(roadmapDst)) {
83
+ fs.copyFileSync(roadmapSrc, roadmapDst);
85
84
  }
86
85
 
87
- // Link references/ extension directory so SKILL.md paths resolve correctly
88
- // (mcp/server.js, lib/core/*, dashboard/, bin/ etc.)
86
+ // ── Ensure references/ junction exists (runs even for existing installations) ──
89
87
  const refsTarget = path.join(skillTarget, 'references');
90
- if (!fs.existsSync(refsTarget)) {
88
+ const refsIsJunction = _isSymlinkOrJunction(refsTarget);
89
+ const refsIsPlainDir = !refsIsJunction && fs.existsSync(refsTarget);
90
+ const refsMissingRuntime = refsIsPlainDir && !fs.existsSync(path.join(refsTarget, 'mcp'));
91
+
92
+ if (!fs.existsSync(refsTarget) || refsMissingRuntime) {
93
+ // Remove old plain directory if it only has docs (no runtime)
94
+ if (refsMissingRuntime) {
95
+ try { fs.rmSync(refsTarget, { recursive: true, force: true }); } catch { /* ok */ }
96
+ }
91
97
  try {
92
98
  fs.symlinkSync(extRoot, refsTarget, 'junction');
93
99
  actions.push('references/ linked');
94
100
  } catch {
95
- // junction failed (rare) — fall back to copying essential docs only
96
101
  fs.mkdirSync(refsTarget, { recursive: true });
97
102
  _copyDocFiles(skillSrc, refsTarget);
98
103
  }
99
104
  }
100
105
 
101
- // Copy package.json so `require('../../package.json')` in source mode works
102
- const pkgSrc = path.join(extRoot, '..', '..', 'package.json');
106
+ // ── Ensure package.json exists ──
103
107
  const pkgDst = path.join(skillTarget, 'package.json');
104
- if (fs.existsSync(pkgSrc) && !fs.existsSync(pkgDst)) {
105
- fs.copyFileSync(pkgSrc, pkgDst);
108
+ if (!fs.existsSync(pkgDst)) {
109
+ const pkgSrc = path.join(extRoot, '..', '..', 'package.json');
110
+ const guardVer = path.join(extRoot, 'guard-version.json');
111
+ if (fs.existsSync(pkgSrc)) {
112
+ fs.copyFileSync(pkgSrc, pkgDst);
113
+ } else if (fs.existsSync(guardVer)) {
114
+ fs.copyFileSync(guardVer, pkgDst);
115
+ }
106
116
  }
107
117
 
108
118
  return actions;
109
119
  }
110
120
 
121
+ function _isSymlinkOrJunction(p) {
122
+ try {
123
+ const stat = fs.lstatSync(p);
124
+ return stat.isSymbolicLink();
125
+ } catch { return false; }
126
+ }
127
+
111
128
  function _copyDocFiles(skillSrc, refsTarget) {
112
129
  const docs = [
113
130
  'config-reference.md', 'config-reference.zh-CN.md',
@@ -2,7 +2,7 @@
2
2
  "name": "cursor-guard-ide",
3
3
  "displayName": "Cursor Guard",
4
4
  "description": "AI code protection dashboard embedded in your IDE — real-time alerts, backup history, one-click snapshots",
5
- "version": "4.8.2",
5
+ "version": "4.8.5",
6
6
  "publisher": "zhangqiang8vipp",
7
7
  "license": "BUSL-1.1",
8
8
  "engines": {