cursor-guard 4.0.0 → 4.1.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.
package/README.md CHANGED
@@ -29,40 +29,47 @@ When Cursor's AI agent edits your files, there's a risk of accidental overwrites
29
29
 
30
30
  ## Installation
31
31
 
32
- ### Method 1: npm
32
+ ### Method 1: npm (Recommended)
33
33
 
34
34
  ```bash
35
35
  npm install cursor-guard
36
+ npx cursor-guard-init
36
37
  ```
37
38
 
38
- After installation, copy the skill files to your Cursor skills directory:
39
+ The `init` command copies skill files to `.cursor/skills/cursor-guard/`, installs MCP dependencies, and adds `.gitignore` entries — all in one step.
39
40
 
40
- **Windows (PowerShell):**
41
+ Options:
41
42
 
42
- ```powershell
43
- # Global (all projects)
44
- Copy-Item -Recurse node_modules/cursor-guard "$env:USERPROFILE/.cursor/skills/cursor-guard"
45
-
46
- # Per-project (current project only)
47
- Copy-Item -Recurse node_modules/cursor-guard .cursor/skills/cursor-guard
43
+ ```bash
44
+ npx cursor-guard-init # project-local (default)
45
+ npx cursor-guard-init --global # global (~/.cursor/skills/)
46
+ npx cursor-guard-init --path /my/project # specify project root
48
47
  ```
49
48
 
50
- **macOS / Linux:**
49
+ After init, the npm package in `node_modules` is no longer needed:
51
50
 
52
51
  ```bash
53
- # Global
54
- cp -r node_modules/cursor-guard ~/.cursor/skills/cursor-guard
55
-
56
- # Per-project
57
- cp -r node_modules/cursor-guard .cursor/skills/cursor-guard
52
+ npm uninstall cursor-guard
58
53
  ```
59
54
 
60
- After copying, you can remove the npm dependency if you don't need it in `node_modules`:
55
+ <details>
56
+ <summary>Manual installation (without init command)</summary>
57
+
58
+ If you prefer manual setup, copy files then install dependencies:
61
59
 
62
60
  ```bash
63
- npm uninstall cursor-guard
61
+ # Copy
62
+ cp -r node_modules/cursor-guard .cursor/skills/cursor-guard
63
+
64
+ # Install MCP dependencies in the skill directory
65
+ cd .cursor/skills/cursor-guard && npm install --omit=dev && cd -
66
+
67
+ # Add to .gitignore so node_modules aren't captured by git snapshots
68
+ echo ".cursor/skills/**/node_modules/" >> .gitignore
64
69
  ```
65
70
 
71
+ </details>
72
+
66
73
  ### Method 2: Git clone
67
74
 
68
75
  ```bash
package/README.zh-CN.md CHANGED
@@ -29,40 +29,47 @@
29
29
 
30
30
  ## 安装
31
31
 
32
- ### 方式一:npm 安装
32
+ ### 方式一:npm 安装(推荐)
33
33
 
34
34
  ```bash
35
35
  npm install cursor-guard
36
+ npx cursor-guard-init
36
37
  ```
37
38
 
38
- 安装后,将技能文件复制到 Cursor 技能目录:
39
+ `init` 命令一键完成:复制技能文件到 `.cursor/skills/cursor-guard/`、安装 MCP 依赖、添加 `.gitignore` 条目。
39
40
 
40
- **Windows (PowerShell):**
41
+ 可选参数:
41
42
 
42
- ```powershell
43
- # 全局安装(所有项目生效)
44
- Copy-Item -Recurse node_modules/cursor-guard "$env:USERPROFILE/.cursor/skills/cursor-guard"
45
-
46
- # 项目级安装(仅当前项目生效)
47
- Copy-Item -Recurse node_modules/cursor-guard .cursor/skills/cursor-guard
43
+ ```bash
44
+ npx cursor-guard-init # 项目级安装(默认)
45
+ npx cursor-guard-init --global # 全局安装(~/.cursor/skills/)
46
+ npx cursor-guard-init --path /my/project # 指定项目根目录
48
47
  ```
49
48
 
50
- **macOS / Linux:**
49
+ 初始化完成后,`node_modules` 中的 npm 包已不再需要:
51
50
 
52
51
  ```bash
53
- # 全局安装
54
- cp -r node_modules/cursor-guard ~/.cursor/skills/cursor-guard
55
-
56
- # 项目级安装
57
- cp -r node_modules/cursor-guard .cursor/skills/cursor-guard
52
+ npm uninstall cursor-guard
58
53
  ```
59
54
 
60
- 复制完成后,如果不需要保留在 `node_modules` 中,可以卸载:
55
+ <details>
56
+ <summary>手动安装(不使用 init 命令)</summary>
57
+
58
+ 如果你更喜欢手动操作,复制文件后需要手动安装依赖:
61
59
 
62
60
  ```bash
63
- npm uninstall cursor-guard
61
+ # 复制
62
+ cp -r node_modules/cursor-guard .cursor/skills/cursor-guard
63
+
64
+ # 在 skill 目录中安装 MCP 依赖
65
+ cd .cursor/skills/cursor-guard && npm install --omit=dev && cd -
66
+
67
+ # 添加到 .gitignore,防止 node_modules 被 git 快照捕获
68
+ echo ".cursor/skills/**/node_modules/" >> .gitignore
64
69
  ```
65
70
 
71
+ </details>
72
+
66
73
  ### 方式二:Git 克隆
67
74
 
68
75
  ```bash
package/SKILL.md CHANGED
@@ -202,7 +202,7 @@ git update-ref refs/guard/snapshot $commit
202
202
 
203
203
  1. **Quick git init** (preferred):
204
204
  ```
205
- git init && git add -A && git commit -m "guard: initial snapshot" --no-verify
205
+ git init; git add -A; git commit -m "guard: initial snapshot" --no-verify
206
206
  ```
207
207
  2. **Shadow copy** (fallback if user declines git):
208
208
  - Copy the target file(s) to `.cursor-guard-backup/<timestamp>/` via Shell.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-guard",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
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",
@@ -27,6 +27,7 @@
27
27
  "test": "node references/lib/utils.test.js && node references/lib/core/core.test.js && node references/mcp/mcp.test.js"
28
28
  },
29
29
  "bin": {
30
+ "cursor-guard-init": "references/bin/cursor-guard-init.js",
30
31
  "cursor-guard-backup": "references/bin/cursor-guard-backup.js",
31
32
  "cursor-guard-doctor": "references/bin/cursor-guard-doctor.js",
32
33
  "cursor-guard-mcp": "references/mcp/server.js"
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { execFileSync } = require('child_process');
8
+
9
+ const args = process.argv.slice(2);
10
+ if (args.includes('--help') || args.includes('-h')) {
11
+ console.log(`Usage: cursor-guard-init [options]
12
+
13
+ Installs cursor-guard skill into your Cursor skills directory, including
14
+ MCP dependencies and .gitignore entries.
15
+
16
+ Options:
17
+ --global Install to ~/.cursor/skills/ (default: project-local)
18
+ --path <dir> Project directory (default: current dir)
19
+ --help, -h Show this help message
20
+ --version, -v Show version number`);
21
+ process.exit(0);
22
+ }
23
+
24
+ if (args.includes('--version') || args.includes('-v')) {
25
+ const pkg = require('../../package.json');
26
+ console.log(pkg.version);
27
+ process.exit(0);
28
+ }
29
+
30
+ const isGlobal = args.includes('--global');
31
+ const pathIdx = args.indexOf('--path');
32
+ const projectDir = path.resolve(pathIdx >= 0 && args[pathIdx + 1] ? args[pathIdx + 1] : '.');
33
+
34
+ const skillSource = path.resolve(__dirname, '../..');
35
+ const skillTarget = isGlobal
36
+ ? path.join(process.env.USERPROFILE || process.env.HOME || os.homedir(), '.cursor', 'skills', 'cursor-guard')
37
+ : path.join(projectDir, '.cursor', 'skills', 'cursor-guard');
38
+
39
+ function copyRecursive(src, dest) {
40
+ const stat = fs.statSync(src);
41
+ if (stat.isDirectory()) {
42
+ if (path.basename(src) === 'node_modules') return;
43
+ if (path.basename(src) === '.git') return;
44
+ fs.mkdirSync(dest, { recursive: true });
45
+ for (const entry of fs.readdirSync(src)) {
46
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
47
+ }
48
+ } else {
49
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
50
+ fs.copyFileSync(src, dest);
51
+ }
52
+ }
53
+
54
+ console.log(`\n cursor-guard init\n`);
55
+ console.log(` Source: ${skillSource}`);
56
+ console.log(` Target: ${skillTarget}`);
57
+ console.log(` Mode: ${isGlobal ? 'global (~/.cursor/skills/)' : 'project-local (.cursor/skills/)'}\n`);
58
+
59
+ // Step 1: Copy skill files (excluding node_modules and .git)
60
+ console.log(' [1/4] Copying skill files...');
61
+ if (fs.existsSync(skillTarget)) {
62
+ fs.rmSync(skillTarget, { recursive: true, force: true });
63
+ }
64
+ copyRecursive(skillSource, skillTarget);
65
+ console.log(' Done.');
66
+
67
+ // Step 2: Install MCP dependencies in skill directory
68
+ console.log(' [2/4] Installing MCP dependencies...');
69
+ try {
70
+ execFileSync('npm', ['install', '--omit=dev', '--ignore-scripts'], {
71
+ cwd: skillTarget,
72
+ stdio: 'pipe',
73
+ shell: process.platform === 'win32',
74
+ });
75
+ console.log(' Done.');
76
+ } catch (e) {
77
+ console.log(` Warning: npm install failed (${e.message}). MCP tools may not work.`);
78
+ console.log(' You can fix this later: cd "' + skillTarget + '" ; npm install');
79
+ }
80
+
81
+ // Step 3: Add .gitignore entries for skill node_modules
82
+ console.log(' [3/4] Updating .gitignore...');
83
+ const gitignorePath = path.join(projectDir, '.gitignore');
84
+ const entries = ['.cursor/skills/**/node_modules/'];
85
+ let gitignoreUpdated = false;
86
+ if (!isGlobal) {
87
+ let existing = '';
88
+ try { existing = fs.readFileSync(gitignorePath, 'utf-8'); } catch { /* doesn't exist */ }
89
+ const missing = entries.filter(e => !existing.includes(e));
90
+ if (missing.length > 0) {
91
+ const newline = existing.endsWith('\n') || !existing ? '' : '\n';
92
+ fs.appendFileSync(gitignorePath, `${newline}# cursor-guard skill dependencies\n${missing.join('\n')}\n`);
93
+ gitignoreUpdated = true;
94
+ console.log(' Added: ' + missing.join(', '));
95
+ } else {
96
+ console.log(' Already configured.');
97
+ }
98
+ } else {
99
+ console.log(' Skipped (global install, not inside a project).');
100
+ }
101
+
102
+ // Step 4: Summary
103
+ console.log(' [4/4] Verifying...');
104
+ const serverExists = fs.existsSync(path.join(skillTarget, 'references', 'mcp', 'server.js'));
105
+ const sdkExists = fs.existsSync(path.join(skillTarget, 'node_modules', '@modelcontextprotocol', 'sdk'));
106
+ const skillMdExists = fs.existsSync(path.join(skillTarget, 'SKILL.md'));
107
+
108
+ console.log(` SKILL.md: ${skillMdExists ? 'OK' : 'MISSING'}`);
109
+ console.log(` MCP server: ${serverExists ? 'OK' : 'MISSING'}`);
110
+ console.log(` MCP SDK: ${sdkExists ? 'OK' : 'MISSING — run npm install in skill dir'}`);
111
+
112
+ console.log(`\n Installation complete!\n`);
113
+ console.log(' Next steps:');
114
+ console.log(' 1. The skill activates automatically in Cursor Agent conversations.');
115
+ console.log(' 2. (Optional) Copy example config to project root:');
116
+ console.log(` cp "${path.join(skillTarget, 'references', 'cursor-guard.example.json')}" .cursor-guard.json`);
117
+ console.log(' 3. (Optional) Enable MCP — add to .cursor/mcp.json:');
118
+ console.log(` { "mcpServers": { "cursor-guard": { "command": "node", "args": ["${path.join(skillTarget, 'references', 'mcp', 'server.js').replace(/\\/g, '/')}"] } } }`);
119
+ console.log(' 4. (Optional) Start auto-backup:');
120
+ console.log(` npx cursor-guard-backup --path "${projectDir}"\n`);
@@ -217,17 +217,35 @@ function runDiagnostics(projectDir) {
217
217
 
218
218
  let mcpSdkAvailable = false;
219
219
  let mcpSdkVersion = null;
220
- try {
221
- const mcpPkgPath = require.resolve('@modelcontextprotocol/sdk/package.json');
222
- const mcpPkg = JSON.parse(fs.readFileSync(mcpPkgPath, 'utf-8'));
223
- mcpSdkAvailable = true;
224
- mcpSdkVersion = mcpPkg.version;
225
- } catch { /* not installed */ }
220
+ // Try resolving SDK from the skill package's own node_modules first,
221
+ // then fall back to the running process's require paths.
222
+ const skillRoot = path.resolve(__dirname, '../../..');
223
+ const sdkCandidates = [
224
+ path.join(skillRoot, 'node_modules', '@modelcontextprotocol', 'sdk', 'package.json'),
225
+ ];
226
+ for (const candidate of sdkCandidates) {
227
+ try {
228
+ if (fs.existsSync(candidate)) {
229
+ const mcpPkg = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
230
+ mcpSdkAvailable = true;
231
+ mcpSdkVersion = mcpPkg.version;
232
+ break;
233
+ }
234
+ } catch { /* ignore */ }
235
+ }
236
+ if (!mcpSdkAvailable) {
237
+ try {
238
+ const mcpPkgPath = require.resolve('@modelcontextprotocol/sdk/package.json');
239
+ const mcpPkg = JSON.parse(fs.readFileSync(mcpPkgPath, 'utf-8'));
240
+ mcpSdkAvailable = true;
241
+ mcpSdkVersion = mcpPkg.version;
242
+ } catch { /* not installed */ }
243
+ }
226
244
 
227
245
  if (mcpServerExists && mcpSdkAvailable) {
228
246
  check('MCP server', 'PASS', `server.js found, SDK ${mcpSdkVersion}`);
229
247
  } else if (mcpServerExists && !mcpSdkAvailable) {
230
- check('MCP server', 'WARN', 'server.js found but @modelcontextprotocol/sdk not installed — run npm install');
248
+ check('MCP server', 'WARN', 'server.js found but @modelcontextprotocol/sdk not installed — run: cd <skill-dir> && npm install');
231
249
  } else if (!mcpServerExists && mcpSdkAvailable) {
232
250
  check('MCP server', 'WARN', `SDK installed (${mcpSdkVersion}) but server.js not found at expected path`);
233
251
  } else {
@@ -121,13 +121,13 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
121
121
 
122
122
  git(['update-ref', branchRef, commitHash], { cwd });
123
123
 
124
- let fileCount = 0;
124
+ const lsOut = git(['ls-tree', '--name-only', '-r', newTree], { cwd, allowFail: true });
125
+ const fileCount = lsOut ? lsOut.split('\n').filter(Boolean).length : 0;
126
+
127
+ let changedCount;
125
128
  if (parentTree) {
126
129
  const diff = git(['diff-tree', '--no-commit-id', '--name-only', '-r', parentTree, newTree], { cwd, allowFail: true });
127
- fileCount = diff ? diff.split('\n').filter(Boolean).length : 0;
128
- } else {
129
- const all = git(['ls-tree', '--name-only', '-r', newTree], { cwd, allowFail: true });
130
- fileCount = all ? all.split('\n').filter(Boolean).length : 0;
130
+ changedCount = diff ? diff.split('\n').filter(Boolean).length : 0;
131
131
  }
132
132
 
133
133
  return {
@@ -135,6 +135,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
135
135
  commitHash,
136
136
  shortHash: commitHash.substring(0, 7),
137
137
  fileCount,
138
+ changedCount,
138
139
  secretsExcluded: secretsExcluded.length > 0 ? secretsExcluded : undefined,
139
140
  };
140
141
  } catch (e) {
@@ -91,12 +91,20 @@ server.tool(
91
91
  path: z.string().describe('Absolute path to the project directory'),
92
92
  strategy: z.enum(['git', 'shadow', 'both']).optional().describe('Backup strategy (default: from config, or "git")'),
93
93
  message: z.string().optional().describe('Custom commit message for git snapshot'),
94
+ scope: z.enum(['protected', 'all']).optional().describe('Snapshot scope: "protected" = only files matching protect patterns (default when protect is configured); "all" = all files regardless of protect config'),
94
95
  },
95
- async ({ path: projectPath, strategy, message }) => {
96
+ async ({ path: projectPath, strategy, message, scope }) => {
96
97
  const resolved = path.resolve(projectPath);
97
98
  const { loadConfig } = require('../lib/utils');
98
99
  const { cfg } = loadConfig(resolved);
99
100
 
101
+ if (scope === 'all') {
102
+ cfg.protect = [];
103
+ } else if (scope === 'protected' && cfg.protect.length === 0) {
104
+ // "protected" requested but no protect patterns configured — snapshot all
105
+ // (no way to filter without patterns)
106
+ }
107
+
100
108
  const effectiveStrategy = strategy || cfg.backup_strategy || 'git';
101
109
  const results = {};
102
110