cursor-guard 4.3.0 → 4.3.1

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/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.1",
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",
@@ -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') {
@@ -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