cursor-guard 4.3.0 → 4.3.2

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
@@ -375,6 +375,50 @@ The skill activates on these signals:
375
375
 
376
376
  ---
377
377
 
378
+ ## Changelog
379
+
380
+ ### v4.3.2
381
+
382
+ - **Fix**: `cursor-guard-init` now adds `node_modules/` (root-level) to `.gitignore` — prevents `git add -A` from scanning thousands of npm dependency files after `npm install cursor-guard --save-dev`
383
+ - **Improve**: Doctor "MCP version" mismatch warning now includes the reload keybinding (`Ctrl+Shift+P -> "Developer: Reload Window"`) for faster action
384
+
385
+ ### v4.3.1
386
+
387
+ - **Fix**: `restore_project` now protects `.gitignore` — added to `GUARD_CONFIGS` so it is restored from HEAD after recovery, preventing post-restore full-tree scans (2500+ files)
388
+ - **Fix**: `cursor-guard-index.lock` cleanup — `createGitSnapshot` now removes stale `.lock` files on entry and in the `finally` block, preventing lock file remnants from blocking subsequent operations
389
+ - **Improve**: Auto-backup summary now filtered by `protect`/`ignore` patterns, excluding `.cursor/skills/` and other non-protected files
390
+ - **Improve**: Summary format changed from flat `M file1, A file2` to categorized `Modified 3: a.js; Added 1: b.js` with i18n support
391
+ - **Improve**: Manual snapshot `message` (from `snapshot_now`) now displayed in dashboard backup table and restore-point drawer
392
+ - **Improve**: SKILL.md adds best-practice guidance for AI agents to provide descriptive `message` when calling `snapshot_now`
393
+
394
+ ### v4.3.0
395
+
396
+ - **Feature**: Backup context metadata — structured Git commit messages with `Files-Changed`, `Summary`, and `Trigger` trailers
397
+ - **Feature**: `listBackups` parses commit trailers and returns `filesChanged`, `summary`, `trigger` fields
398
+ - **Feature**: Dashboard backup table adds "Changes" column; restore-point drawer shows trigger, files changed, and summary
399
+
400
+ ### v4.2.2
401
+
402
+ - **Fix**: `restore_project` now protects `.cursor-guard.json` during restore (prevents config loss)
403
+ - **Fix**: Post-restore HEAD recovery loop extended to restore both `.cursor/` and `.cursor-guard.json`
404
+ - **Improve**: `cursor-guard-init` now reminds users to `git commit` after installation in Git repos
405
+
406
+ ### v4.2.1
407
+
408
+ - **Fix**: `t()` function uses `replaceAll` for i18n placeholder substitution
409
+ - **Fix**: Removed unused `loadActiveAlert` import from dashboard server
410
+ - **Fix**: Added `git-snapshot` type to dashboard filter bar
411
+ - **Fix**: Replaced `&&` with `;` in `detail.mcp_no_sdk` i18n string for cross-platform compatibility
412
+ - **Fix**: Deduplicated `sdkCandidates` in `doctor.js`
413
+
414
+ ### v4.2.0
415
+
416
+ - **Feature**: Web dashboard — local read-only UI with health overview, backup table, restore-point drawers, diagnostics, protection scope
417
+ - **Feature**: Dual-language (zh-CN / en-US) with full i18n coverage including doctor checks, health issues, alert messages
418
+ - **Feature**: Multi-project support via CLI `--path` args and frontend project selector
419
+
420
+ ---
421
+
378
422
  ## Known Limitations
379
423
 
380
424
  - **Binary files**: Git diffs and snapshots work on text files. Binary files (images, compiled assets) are stored but cannot be meaningfully diffed or partially restored.
package/README.zh-CN.md CHANGED
@@ -375,6 +375,50 @@ node references\dashboard\server.js --path "D:\MyProject"
375
375
 
376
376
  ---
377
377
 
378
+ ## 更新日志
379
+
380
+ ### v4.3.2
381
+
382
+ - **修复**:`cursor-guard-init` 现在会将根目录 `node_modules/` 加入 `.gitignore`——防止 `npm install cursor-guard --save-dev` 后 `git add -A` 扫描数千个依赖文件导致极度缓慢
383
+ - **改进**:Doctor "MCP version" 版本不一致警告现在包含重载快捷键提示(`Ctrl+Shift+P -> "Developer: Reload Window"`),方便快速操作
384
+
385
+ ### v4.3.1
386
+
387
+ - **修复**:`restore_project` 现在保护 `.gitignore`——加入 `GUARD_CONFIGS`,恢复后从 HEAD 还原,防止 `.gitignore` 丢失导致全量扫描(2500+ 文件)
388
+ - **修复**:`cursor-guard-index.lock` 清理——`createGitSnapshot` 在入口和 `finally` 块中清除过期 `.lock` 文件,防止锁文件残留阻塞后续操作
389
+ - **改进**:自动备份 summary 现按 `protect`/`ignore` 模式过滤,排除 `.cursor/skills/` 等非保护文件
390
+ - **改进**:summary 格式从扁平的 `M file1, A file2` 改为分类格式 `修改 3: a.js; 新增 1: b.js`,支持中英双语
391
+ - **改进**:手动快照的 `message`(来自 `snapshot_now`)现显示在仪表盘备份表格和恢复点抽屉中
392
+ - **改进**:SKILL.md 新增最佳实践指引,建议 AI agent 在调用 `snapshot_now` 时传入描述性 `message`
393
+
394
+ ### v4.3.0
395
+
396
+ - **功能**:备份上下文元数据——Git commit 消息使用结构化 trailer(`Files-Changed`、`Summary`、`Trigger`)
397
+ - **功能**:`listBackups` 解析 commit trailer,返回 `filesChanged`、`summary`、`trigger` 字段
398
+ - **功能**:仪表盘备份表格新增"变更"列;恢复点抽屉展示触发方式、变更文件数、变更摘要
399
+
400
+ ### v4.2.2
401
+
402
+ - **修复**:`restore_project` 恢复时保护 `.cursor-guard.json`(防止配置丢失)
403
+ - **修复**:恢复后 HEAD 恢复循环扩展为同时还原 `.cursor/` 和 `.cursor-guard.json`
404
+ - **改进**:`cursor-guard-init` 在 Git 仓库中安装后提醒用户执行 `git commit`
405
+
406
+ ### v4.2.1
407
+
408
+ - **修复**:`t()` 函数改用 `replaceAll` 替换 i18n 占位符
409
+ - **修复**:移除仪表盘 server 中未使用的 `loadActiveAlert` 导入
410
+ - **修复**:仪表盘过滤栏补充 `git-snapshot` 类型选项
411
+ - **修复**:`detail.mcp_no_sdk` i18n 字符串中 `&&` 替换为 `;`,确保跨平台兼容
412
+ - **修复**:`doctor.js` 中 `sdkCandidates` 去重
413
+
414
+ ### v4.2.0
415
+
416
+ - **功能**:Web 仪表盘——本地只读 UI,健康总览、备份表格、恢复点抽屉、诊断、保护范围
417
+ - **功能**:中英双语(zh-CN / en-US),完整 i18n 覆盖含 doctor 检查项、健康问题、告警消息
418
+ - **功能**:多项目支持——CLI `--path` 参数 + 前端项目选择器
419
+
420
+ ---
421
+
378
422
  ## 已知限制
379
423
 
380
424
  - **二进制文件**:Git 快照可以存储二进制文件(图片、编译产物),但无法进行有意义的 diff 或部分恢复。
package/SKILL.md CHANGED
@@ -146,6 +146,15 @@ When the target file of an edit **falls outside the protected scope**, the agent
146
146
  **Before any High-risk operation on a protected file:**
147
147
 
148
148
  > **MCP shortcut**: if `snapshot_now` tool is available, call it with `{ "path": "<project>", "strategy": "git" }` instead of the shell commands below. The tool handles temp index, secrets exclusion, and ref creation internally, and returns `{ "git": { "status": "created", "commitHash": "...", "shortHash": "..." } }`. Report the `shortHash` to the user and proceed.
149
+ >
150
+ > **Best practice — descriptive messages**: Always provide a meaningful `message` parameter that describes *why* this snapshot is being created and *what* changes are at risk. This message appears in the dashboard restore-point list, helping users identify which snapshot to restore from. Example:
151
+ > ```json
152
+ > {
153
+ > "path": "/project",
154
+ > "strategy": "git",
155
+ > "message": "guard: before refactoring auth middleware — moving session logic from app.js to middleware/auth.js"
156
+ > }
157
+ > ```
149
158
 
150
159
  Use a **temporary index and dedicated ref** so the user's staged/unstaged state is never touched:
151
160
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-guard",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
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",
@@ -81,7 +81,7 @@ try {
81
81
  // Step 3: Add .gitignore entries for skill node_modules
82
82
  console.log(' [3/4] Updating .gitignore...');
83
83
  const gitignorePath = path.join(projectDir, '.gitignore');
84
- const entries = ['.cursor/skills/**/node_modules/'];
84
+ const entries = ['node_modules/', '.cursor/skills/**/node_modules/'];
85
85
  let gitignoreUpdated = false;
86
86
  if (!isGlobal) {
87
87
  let existing = '';
@@ -95,6 +95,11 @@ const I18N = {
95
95
  'trigger.manual': 'Manual (agent)',
96
96
  'trigger.pre-restore': 'Pre-Restore',
97
97
  'backups.col.summary': 'Changes',
98
+ 'summary.modified': 'Modified',
99
+ 'summary.added': 'Added',
100
+ 'summary.deleted': 'Deleted',
101
+ 'summary.renamed': 'Renamed',
102
+ 'summary.files': 'files',
98
103
 
99
104
  'error.fetchFailed': 'Failed to fetch data',
100
105
  'error.sectionFailed': 'This section failed to load',
@@ -268,6 +273,11 @@ const I18N = {
268
273
  'trigger.manual': '手动(Agent)',
269
274
  'trigger.pre-restore': '恢复前快照',
270
275
  'backups.col.summary': '变更',
276
+ 'summary.modified': '修改',
277
+ 'summary.added': '新增',
278
+ 'summary.deleted': '删除',
279
+ 'summary.renamed': '重命名',
280
+ 'summary.files': '个文件',
271
281
 
272
282
  'error.fetchFailed': '数据拉取失败',
273
283
  'error.sectionFailed': '此区块加载失败',
@@ -793,14 +803,28 @@ function renderFilterBar() {
793
803
  ).join('');
794
804
  }
795
805
 
806
+ function translateSummary(raw) {
807
+ if (!raw) return raw;
808
+ return raw
809
+ .replace(/\bModified (\d+)/g, (_, n) => `${t('summary.modified')} ${n}`)
810
+ .replace(/\bAdded (\d+)/g, (_, n) => `${t('summary.added')} ${n}`)
811
+ .replace(/\bDeleted (\d+)/g, (_, n) => `${t('summary.deleted')} ${n}`)
812
+ .replace(/\bRenamed (\d+)/g, (_, n) => `${t('summary.renamed')} ${n}`);
813
+ }
814
+
796
815
  function formatSummaryCell(b) {
797
816
  const parts = [];
798
- if (b.filesChanged != null) parts.push(`<span class="text-sm">${b.filesChanged} files</span>`);
817
+ if (b.filesChanged != null) parts.push(`<span class="text-sm">${b.filesChanged} ${t('summary.files')}</span>`);
799
818
  if (b.trigger) parts.push(`<span class="badge badge-trigger">${t('trigger.' + b.trigger)}</span>`);
800
819
  if (b.summary) {
801
- const short = b.summary.length > 60 ? b.summary.substring(0, 57) + '...' : b.summary;
820
+ const translated = translateSummary(b.summary);
821
+ const short = translated.length > 80 ? translated.substring(0, 77) + '...' : translated;
802
822
  parts.push(`<span class="text-muted text-sm">${esc(short)}</span>`);
803
823
  }
824
+ if (b.message && !b.message.startsWith('guard:')) {
825
+ const msgShort = b.message.length > 50 ? b.message.substring(0, 47) + '...' : b.message;
826
+ parts.push(`<span class="text-muted text-sm">${esc(msgShort)}</span>`);
827
+ }
804
828
  return parts.length > 0 ? parts.join(' ') : '<span class="text-muted text-sm">-</span>';
805
829
  }
806
830
 
@@ -929,7 +953,7 @@ function openRestoreDrawer(backup) {
929
953
  if (backup.commitHash) fields.push({ key: 'drawer.field.hash', val: backup.commitHash });
930
954
  if (backup.path) fields.push({ key: 'drawer.field.path', val: backup.path });
931
955
  if (backup.message) fields.push({ key: 'drawer.field.message', val: backup.message });
932
- if (backup.summary) fields.push({ key: 'drawer.field.summary', val: backup.summary });
956
+ if (backup.summary) fields.push({ key: 'drawer.field.summary', val: translateSummary(backup.summary) });
933
957
 
934
958
  const refText = backup.ref || backup.shortHash || backup.timestamp || '';
935
959
  const jsonText = JSON.stringify(backup, null, 2);
@@ -262,8 +262,37 @@ async function runBackup(projectDir, intervalOverride) {
262
262
  if ((cfg.backup_strategy === 'git' || cfg.backup_strategy === 'both') && repo) {
263
263
  const context = { trigger: 'auto', changedFileCount };
264
264
  if (porcelain) {
265
- const pLines = porcelain.split('\n').filter(Boolean).slice(0, 20);
266
- context.summary = pLines.map(l => l.substring(0, 2).trim() + ' ' + l.substring(3)).join(', ');
265
+ let pLines = porcelain.split('\n').filter(Boolean);
266
+ if (cfg.protect.length > 0 || cfg.ignore.length > 0) {
267
+ pLines = pLines.filter(line => {
268
+ const filePart = line.substring(3);
269
+ const arrowIdx = filePart.indexOf(' -> ');
270
+ const raw = arrowIdx >= 0 ? filePart.substring(arrowIdx + 4) : filePart;
271
+ const rel = unquoteGitPath(raw);
272
+ const fakeFile = { rel, full: path.join(projectDir, rel) };
273
+ return filterFiles([fakeFile], cfg).length > 0;
274
+ });
275
+ }
276
+ if (pLines.length > 0) {
277
+ const groups = { M: [], A: [], D: [], R: [] };
278
+ for (const l of pLines) {
279
+ const code = l.substring(0, 2).trim();
280
+ const filePart = l.substring(3);
281
+ const arrowIdx = filePart.indexOf(' -> ');
282
+ const file = arrowIdx >= 0 ? filePart.substring(arrowIdx + 4) : filePart;
283
+ const key = code.startsWith('R') ? 'R'
284
+ : code.includes('D') ? 'D'
285
+ : code.includes('A') || code === '??' ? 'A'
286
+ : 'M';
287
+ groups[key].push(file);
288
+ }
289
+ const parts = [];
290
+ if (groups.M.length) parts.push(`Modified ${groups.M.length}: ${groups.M.slice(0, 5).join(', ')}`);
291
+ if (groups.A.length) parts.push(`Added ${groups.A.length}: ${groups.A.slice(0, 5).join(', ')}`);
292
+ if (groups.D.length) parts.push(`Deleted ${groups.D.length}: ${groups.D.slice(0, 5).join(', ')}`);
293
+ if (groups.R.length) parts.push(`Renamed ${groups.R.length}: ${groups.R.slice(0, 5).join(', ')}`);
294
+ context.summary = parts.join('; ');
295
+ }
267
296
  }
268
297
  const snapResult = createGitSnapshot(projectDir, cfg, { branchRef, context });
269
298
  if (snapResult.status === 'created') {
@@ -266,7 +266,7 @@ function runDiagnostics(projectDir) {
266
266
  const memPkg = require('../../../package.json');
267
267
  if (diskPkg.version !== memPkg.version) {
268
268
  check('MCP version', 'WARN',
269
- `running v${memPkg.version} but disk has v${diskPkg.version} — restart Cursor to load the new version`);
269
+ `running v${memPkg.version} but disk has v${diskPkg.version} — restart Cursor (Ctrl+Shift+P -> "Developer: Reload Window") to load the new version`);
270
270
  } else {
271
271
  check('MCP version', 'PASS', `v${memPkg.version}`);
272
272
  }
@@ -21,7 +21,7 @@ function validateRelativePath(file) {
21
21
  const VALID_SHADOW_SOURCE = /^\d{8}_\d{6}(_\d{3})?$|^pre-restore-\d{8}_\d{6}(_\d{3})?$/;
22
22
 
23
23
  const TOOL_DIRS = ['.cursor/', '.cursor\\'];
24
- const GUARD_CONFIGS = ['.cursor-guard.json'];
24
+ const GUARD_CONFIGS = ['.cursor-guard.json', '.gitignore'];
25
25
 
26
26
  function isToolPath(filePath) {
27
27
  const normalized = filePath.replace(/\\/g, '/');
@@ -94,9 +94,11 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
94
94
  if (!gDir) return { status: 'error', error: 'not a git repository' };
95
95
 
96
96
  const guardIndex = path.join(gDir, 'cursor-guard-index');
97
+ const guardIndexLock = guardIndex + '.lock';
97
98
  const env = { ...process.env, GIT_INDEX_FILE: guardIndex };
98
99
 
99
100
  try { fs.unlinkSync(guardIndex); } catch { /* doesn't exist */ }
101
+ try { fs.unlinkSync(guardIndexLock); } catch { /* doesn't exist */ }
100
102
 
101
103
  try {
102
104
  const parentHash = git(['rev-parse', '--verify', branchRef], { cwd, allowFail: true });
@@ -164,6 +166,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
164
166
  return { status: 'error', error: e.message };
165
167
  } finally {
166
168
  try { fs.unlinkSync(guardIndex); } catch { /* ignore */ }
169
+ try { fs.unlinkSync(guardIndexLock); } catch { /* ignore */ }
167
170
  }
168
171
  }
169
172