cursor-guard 4.9.9 → 4.9.15

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.
Files changed (35) hide show
  1. package/README.md +697 -697
  2. package/README.zh-CN.md +696 -696
  3. package/ROADMAP.md +1775 -1720
  4. package/SKILL.md +631 -629
  5. package/docs/RELEASE.md +197 -196
  6. package/docs/SNAPSHOT-BOOKMARK.md +47 -0
  7. package/package.json +70 -69
  8. package/references/dashboard/public/app.js +2079 -1832
  9. package/references/dashboard/public/style.css +1660 -1573
  10. package/references/dashboard/server.js +197 -4
  11. package/references/lib/core/backups.js +509 -492
  12. package/references/lib/core/core.test.js +1761 -1616
  13. package/references/lib/core/snapshot.js +441 -369
  14. package/references/mcp/mcp.test.js +381 -362
  15. package/references/mcp/server.js +404 -347
  16. package/references/vscode-extension/dist/{cursor-guard-ide-4.9.9.vsix → cursor-guard-ide-4.9.15.vsix} +0 -0
  17. package/references/vscode-extension/dist/dashboard/public/app.js +2079 -1832
  18. package/references/vscode-extension/dist/dashboard/public/style.css +1660 -1573
  19. package/references/vscode-extension/dist/dashboard/server.js +197 -4
  20. package/references/vscode-extension/dist/extension.js +780 -704
  21. package/references/vscode-extension/dist/guard-version.json +1 -1
  22. package/references/vscode-extension/dist/lib/auto-setup.js +201 -192
  23. package/references/vscode-extension/dist/lib/core/backups.js +509 -492
  24. package/references/vscode-extension/dist/lib/core/snapshot.js +441 -369
  25. package/references/vscode-extension/dist/lib/poller.js +161 -21
  26. package/references/vscode-extension/dist/lib/sidebar-webview.js +22 -0
  27. package/references/vscode-extension/dist/mcp/server.js +152 -35
  28. package/references/vscode-extension/dist/package.json +7 -1
  29. package/references/vscode-extension/dist/skill/ROADMAP.md +1775 -1720
  30. package/references/vscode-extension/dist/skill/SKILL.md +631 -629
  31. package/references/vscode-extension/extension.js +780 -704
  32. package/references/vscode-extension/lib/auto-setup.js +201 -192
  33. package/references/vscode-extension/lib/poller.js +161 -21
  34. package/references/vscode-extension/lib/sidebar-webview.js +22 -0
  35. package/references/vscode-extension/package.json +146 -140
@@ -1,347 +1,404 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const path = require('path');
5
- const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
6
- const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
7
- const { z } = require('zod');
8
-
9
- const { runDiagnostics } = require('../lib/core/doctor');
10
- const { createGitSnapshot, createShadowCopy } = require('../lib/core/snapshot');
11
- const { listBackups } = require('../lib/core/backups');
12
- const { restoreFile, previewProjectRestore, executeProjectRestore } = require('../lib/core/restore');
13
- const { runFixes } = require('../lib/core/doctor-fix');
14
- const { getBackupStatus } = require('../lib/core/status');
15
- const { getDashboard } = require('../lib/core/dashboard');
16
- const { loadActiveAlert } = require('../lib/core/anomaly');
17
- const { loadActivePreWarnings } = require('../lib/core/pre-warning');
18
-
19
- const { loadConfig, gitDir: getGitDir } = require('../lib/utils');
20
-
21
- const pkg = require('../../package.json');
22
-
23
- // ── Auto-watch manager for always_watch mode ─────────────────
24
-
25
- const watchedProjects = new Map();
26
-
27
- function ensureWatcher(projectPath) {
28
- if (watchedProjects.has(projectPath)) return;
29
- const { cfg, loaded } = loadConfig(projectPath);
30
- if (!loaded || !cfg.always_watch) return;
31
- if (isWatcherRunning(projectPath)) {
32
- watchedProjects.set(projectPath, { pid: null, external: true });
33
- return;
34
- }
35
- try {
36
- const { spawn } = require('child_process');
37
- const watcherScript = path.join(__dirname, '..', 'bin', 'cursor-guard-backup.js');
38
- const child = spawn(process.execPath, [watcherScript, '--path', projectPath], {
39
- detached: true,
40
- stdio: 'ignore',
41
- windowsHide: true,
42
- });
43
- child.unref();
44
- watchedProjects.set(projectPath, { pid: child.pid, external: false });
45
- } catch { /* spawn failed — non-fatal */ }
46
- }
47
-
48
- // ── Alert injection helper ──────────────────────────────────────
49
-
50
- function injectAlert(projectPath, result) {
51
- const alert = loadActiveAlert(projectPath);
52
- if (alert) {
53
- result._activeAlert = {
54
- type: alert.type,
55
- message: alert.recommendation || `${alert.fileCount} files changed in ${alert.windowSeconds}s`,
56
- timestamp: alert.timestamp,
57
- expiresAt: alert.expiresAt,
58
- };
59
- }
60
- return result;
61
- }
62
-
63
- function injectPreWarning(projectPath, result) {
64
- const warnings = loadActivePreWarnings(projectPath);
65
- if (warnings.length > 0) {
66
- result._activePreWarning = {
67
- count: warnings.length,
68
- latest: warnings[0],
69
- };
70
- }
71
- return result;
72
- }
73
-
74
- // ── Watcher status check ────────────────────────────────────────
75
-
76
- function isWatcherRunning(projectDir) {
77
- const fs = require('fs');
78
- const gDir = getGitDir(projectDir);
79
- const lockFile = gDir
80
- ? path.join(gDir, 'cursor-guard.lock')
81
- : path.join(projectDir, '.cursor-guard-backup', 'cursor-guard.lock');
82
- if (!fs.existsSync(lockFile)) return false;
83
- try {
84
- const content = fs.readFileSync(lockFile, 'utf-8');
85
- const pidMatch = content.match(/pid=(\d+)/);
86
- if (pidMatch) {
87
- process.kill(parseInt(pidMatch[1], 10), 0);
88
- return true;
89
- }
90
- } catch { /* pid gone or lock unreadable */ }
91
- return false;
92
- }
93
-
94
- function injectWatcherWarning(projectPath, result) {
95
- if (!isWatcherRunning(projectPath)) {
96
- result._warning = 'Watcher is NOT running — auto-backup protection is inactive. Any file changes made without a manual snapshot_now call will NOT be captured. Consider starting the watcher or calling snapshot_now before making changes.';
97
- }
98
- return result;
99
- }
100
-
101
- // ── Server ──────────────────────────────────────────────────────
102
-
103
- const server = new McpServer({
104
- name: 'cursor-guard',
105
- version: pkg.version,
106
- });
107
-
108
- // ── Tool 1: doctor ──────────────────────────────────────────────
109
-
110
- server.tool(
111
- 'doctor',
112
- 'Run health checks on a project: environment, config, Git, backup refs, shadow copies, disk space. Read-only, safe to call anytime.',
113
- {
114
- path: z.string().describe('Absolute path to the project directory'),
115
- },
116
- async ({ path: projectPath }) => {
117
- const resolved = path.resolve(projectPath);
118
- ensureWatcher(resolved);
119
- const result = injectPreWarning(resolved, injectAlert(resolved, runDiagnostics(resolved)));
120
- injectWatcherWarning(resolved, result);
121
- return {
122
- content: [{
123
- type: 'text',
124
- text: JSON.stringify(result, null, 2),
125
- }],
126
- };
127
- }
128
- );
129
-
130
- // ── Tool 2: list_backups ────────────────────────────────────────
131
-
132
- server.tool(
133
- 'list_backups',
134
- 'List available backup/restore points from all sources (git refs, shadow copies). Read-only. Use before restore to find candidate versions.',
135
- {
136
- path: z.string().describe('Absolute path to the project directory'),
137
- file: z.string().optional().describe('Filter to a specific file (relative path)'),
138
- before: z.string().optional().describe('Only show backups before this time (e.g. "10 minutes ago", "2026-03-21T14:00:00")'),
139
- limit: z.number().optional().describe('Max results per source (default 20)'),
140
- },
141
- async ({ path: projectPath, file, before, limit }) => {
142
- const resolved = path.resolve(projectPath);
143
- ensureWatcher(resolved);
144
- const result = injectPreWarning(resolved, injectAlert(resolved, listBackups(resolved, { file, before, limit })));
145
- return {
146
- content: [{
147
- type: 'text',
148
- text: JSON.stringify(result, null, 2),
149
- }],
150
- };
151
- }
152
- );
153
-
154
- // ── Tool 3: snapshot_now ────────────────────────────────────────
155
-
156
- server.tool(
157
- 'snapshot_now',
158
- 'Create an immediate backup snapshot of the current project state. Use before risky operations to preserve a restore point.',
159
- {
160
- path: z.string().describe('Absolute path to the project directory'),
161
- strategy: z.enum(['git', 'shadow', 'both']).optional().describe('Backup strategy (default: from config, or "git")'),
162
- message: z.string().optional().describe('Custom commit message for git snapshot'),
163
- 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'),
164
- intent: z.string().optional().describe('Why this snapshot is being created — describe the operation about to happen (e.g. "refactoring auth middleware to use JWT")'),
165
- agent: z.string().optional().describe('AI model identifier (e.g. "claude-4-opus")'),
166
- session: z.string().optional().describe('Conversation or session ID for audit trail'),
167
- },
168
- async ({ path: projectPath, strategy, message, scope, intent, agent, session }) => {
169
- const resolved = path.resolve(projectPath);
170
- ensureWatcher(resolved);
171
- const { cfg } = loadConfig(resolved);
172
-
173
- if (scope === 'all') {
174
- cfg.protect = [];
175
- } else if (scope === 'protected' && cfg.protect.length === 0) {
176
- // "protected" requested but no protect patterns configured — snapshot all
177
- // (no way to filter without patterns)
178
- }
179
-
180
- const effectiveStrategy = strategy || cfg.backup_strategy || 'git';
181
- const results = {};
182
-
183
- if (effectiveStrategy === 'git' || effectiveStrategy === 'both') {
184
- const context = { trigger: 'manual' };
185
- if (intent) context.intent = intent;
186
- if (agent) context.agent = agent;
187
- if (session) context.session = session;
188
- results.git = createGitSnapshot(resolved, cfg, {
189
- branchRef: 'refs/guard/snapshot',
190
- message: message || `guard: manual snapshot ${new Date().toISOString()}`,
191
- context,
192
- allowEmptyTree: true,
193
- });
194
- }
195
-
196
- if (effectiveStrategy === 'shadow' || effectiveStrategy === 'both') {
197
- results.shadow = createShadowCopy(resolved, cfg);
198
- }
199
-
200
- injectPreWarning(resolved, injectAlert(resolved, results));
201
- injectWatcherWarning(resolved, results);
202
-
203
- return {
204
- content: [{
205
- type: 'text',
206
- text: JSON.stringify(results, null, 2),
207
- }],
208
- };
209
- }
210
- );
211
-
212
- // ── Tool 4: restore_file ────────────────────────────────────────
213
-
214
- server.tool(
215
- 'restore_file',
216
- 'Restore a single file from a backup source (git commit/ref or shadow copy timestamp). By default, preserves the current version in a pre-restore snapshot before restoring.',
217
- {
218
- path: z.string().describe('Absolute path to the project directory'),
219
- file: z.string().describe('Relative path to the file to restore'),
220
- source: z.string().describe('Backup source: git commit hash, ref name, or shadow copy timestamp (e.g. "20260321_143205")'),
221
- preserve_current: z.boolean().optional().describe('Create pre-restore snapshot before restoring (default true)'),
222
- },
223
- async ({ path: projectPath, file, source, preserve_current }) => {
224
- const resolved = path.resolve(projectPath);
225
- ensureWatcher(resolved);
226
- const result = injectPreWarning(resolved, injectAlert(resolved, restoreFile(resolved, file, source, {
227
- preserveCurrent: preserve_current,
228
- })));
229
- injectWatcherWarning(resolved, result);
230
- return {
231
- content: [{
232
- type: 'text',
233
- text: JSON.stringify(result, null, 2),
234
- }],
235
- };
236
- }
237
- );
238
-
239
- // ── Tool 5: restore_project ─────────────────────────────────────
240
-
241
- server.tool(
242
- 'restore_project',
243
- 'Preview or execute a full project restore to a given backup point. In preview mode (default), shows affected files (including untracked) without changes. In execute mode, creates a pre-restore snapshot then restores all tracked files and cleans untracked files.',
244
- {
245
- path: z.string().describe('Absolute path to the project directory'),
246
- source: z.string().describe('Backup source: git commit hash or ref name'),
247
- preview: z.boolean().optional().describe('If true (default), only show what would change. If false, execute the restore.'),
248
- preserve_current: z.boolean().optional().describe('Create pre-restore snapshot before executing (default true, only used when preview=false)'),
249
- clean_untracked: z.boolean().optional().describe('Remove untracked non-ignored files after restore (default true, only used when preview=false)'),
250
- },
251
- async ({ path: projectPath, source, preview, preserve_current, clean_untracked }) => {
252
- const resolved = path.resolve(projectPath);
253
- ensureWatcher(resolved);
254
-
255
- if (preview !== false) {
256
- const result = injectPreWarning(resolved, injectAlert(resolved, previewProjectRestore(resolved, source)));
257
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
258
- }
259
-
260
- const result = injectPreWarning(resolved, injectAlert(resolved, executeProjectRestore(resolved, source, {
261
- preserveCurrent: preserve_current,
262
- cleanUntracked: clean_untracked,
263
- })));
264
- injectWatcherWarning(resolved, result);
265
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
266
- }
267
- );
268
-
269
- // ── Tool 6: doctor_fix ──────────────────────────────────────────
270
-
271
- server.tool(
272
- 'doctor_fix',
273
- 'Auto-fix common configuration and environment issues: create missing config, init git repo, add .cursor-guard-backup/ to .gitignore, remove stale lock files, fix strategy mismatch. Each fix is idempotent. Use dry_run=true to preview without changes.',
274
- {
275
- path: z.string().describe('Absolute path to the project directory'),
276
- dry_run: z.boolean().optional().describe('If true, report what would be fixed without modifying anything (default false)'),
277
- },
278
- async ({ path: projectPath, dry_run }) => {
279
- const resolved = path.resolve(projectPath);
280
- ensureWatcher(resolved);
281
- const result = injectPreWarning(resolved, injectAlert(resolved, runFixes(resolved, { dryRun: !!dry_run })));
282
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
283
- }
284
- );
285
-
286
- // ── Tool 7: backup_status ───────────────────────────────────────
287
-
288
- server.tool(
289
- 'backup_status',
290
- 'Get comprehensive backup system status: watcher running/stale, last backup time per strategy, configured strategy and retention, guard ref counts, disk space. Read-only, safe to call anytime.',
291
- {
292
- path: z.string().describe('Absolute path to the project directory'),
293
- },
294
- async ({ path: projectPath }) => {
295
- const resolved = path.resolve(projectPath);
296
- ensureWatcher(resolved);
297
- const result = injectPreWarning(resolved, injectAlert(resolved, getBackupStatus(resolved)));
298
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
299
- }
300
- );
301
-
302
- // ── Tool 8: dashboard ───────────────────────────────────────────
303
-
304
- server.tool(
305
- 'dashboard',
306
- 'Get a comprehensive backup health dashboard: strategy, last backup time, backup counts, disk usage breakdown, protection scope, health assessment, and active alerts. Combines status + analytics in one call.',
307
- {
308
- path: z.string().describe('Absolute path to the project directory'),
309
- },
310
- async ({ path: projectPath }) => {
311
- const resolved = path.resolve(projectPath);
312
- ensureWatcher(resolved);
313
- const result = injectPreWarning(resolved, injectAlert(resolved, getDashboard(resolved)));
314
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
315
- }
316
- );
317
-
318
- // ── Tool 9: alert_status ────────────────────────────────────────
319
-
320
- server.tool(
321
- 'alert_status',
322
- 'Check if there is an active change-velocity alert (V4 proactive detection). Returns the alert details if active, or confirms no alert. Read-only, safe to call anytime.',
323
- {
324
- path: z.string().describe('Absolute path to the project directory'),
325
- },
326
- async ({ path: projectPath }) => {
327
- const resolved = path.resolve(projectPath);
328
- ensureWatcher(resolved);
329
- const alert = loadActiveAlert(resolved);
330
- const result = alert
331
- ? { active: true, alert }
332
- : { active: false, message: 'No active alerts' };
333
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
334
- }
335
- );
336
-
337
- // ── Start ───────────────────────────────────────────────────────
338
-
339
- async function main() {
340
- const transport = new StdioServerTransport();
341
- await server.connect(transport);
342
- }
343
-
344
- main().catch((err) => {
345
- console.error('cursor-guard MCP server failed to start:', err);
346
- process.exit(1);
347
- });
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
6
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
7
+ const { z } = require('zod');
8
+
9
+ const { runDiagnostics } = require('../lib/core/doctor');
10
+ const { createGitSnapshot, createShadowCopy, trailerScalar } = require('../lib/core/snapshot');
11
+ const { listBackups } = require('../lib/core/backups');
12
+ const { restoreFile, previewProjectRestore, executeProjectRestore } = require('../lib/core/restore');
13
+ const { runFixes } = require('../lib/core/doctor-fix');
14
+ const { getBackupStatus } = require('../lib/core/status');
15
+ const { getDashboard } = require('../lib/core/dashboard');
16
+ const { loadActiveAlert } = require('../lib/core/anomaly');
17
+ const { loadActivePreWarnings } = require('../lib/core/pre-warning');
18
+
19
+ const { loadConfig, gitDir: getGitDir } = require('../lib/utils');
20
+
21
+ const pkg = require('../../package.json');
22
+
23
+ // ── Auto-watch manager for always_watch mode ─────────────────
24
+
25
+ const watchedProjects = new Map();
26
+
27
+ function ensureWatcher(projectPath) {
28
+ if (watchedProjects.has(projectPath)) return;
29
+ const { cfg, loaded } = loadConfig(projectPath);
30
+ if (!loaded || !cfg.always_watch) return;
31
+ if (isWatcherRunning(projectPath)) {
32
+ watchedProjects.set(projectPath, { pid: null, external: true });
33
+ return;
34
+ }
35
+ try {
36
+ const { spawn } = require('child_process');
37
+ const watcherScript = path.join(__dirname, '..', 'bin', 'cursor-guard-backup.js');
38
+ const child = spawn(process.execPath, [watcherScript, '--path', projectPath], {
39
+ detached: true,
40
+ stdio: 'ignore',
41
+ windowsHide: true,
42
+ });
43
+ child.unref();
44
+ watchedProjects.set(projectPath, { pid: child.pid, external: false });
45
+ } catch { /* spawn failed — non-fatal */ }
46
+ }
47
+
48
+ // ── Alert injection helper ──────────────────────────────────────
49
+
50
+ function injectAlert(projectPath, result) {
51
+ const alert = loadActiveAlert(projectPath);
52
+ if (alert) {
53
+ result._activeAlert = {
54
+ type: alert.type,
55
+ message: alert.recommendation || `${alert.fileCount} files changed in ${alert.windowSeconds}s`,
56
+ timestamp: alert.timestamp,
57
+ expiresAt: alert.expiresAt,
58
+ };
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function injectPreWarning(projectPath, result) {
64
+ const warnings = loadActivePreWarnings(projectPath);
65
+ if (warnings.length > 0) {
66
+ result._activePreWarning = {
67
+ count: warnings.length,
68
+ latest: warnings[0],
69
+ };
70
+ }
71
+ return result;
72
+ }
73
+
74
+ // ── Watcher status check ────────────────────────────────────────
75
+
76
+ function isWatcherRunning(projectDir) {
77
+ const fs = require('fs');
78
+ const gDir = getGitDir(projectDir);
79
+ const lockFile = gDir
80
+ ? path.join(gDir, 'cursor-guard.lock')
81
+ : path.join(projectDir, '.cursor-guard-backup', 'cursor-guard.lock');
82
+ if (!fs.existsSync(lockFile)) return false;
83
+ try {
84
+ const content = fs.readFileSync(lockFile, 'utf-8');
85
+ const pidMatch = content.match(/pid=(\d+)/);
86
+ if (pidMatch) {
87
+ process.kill(parseInt(pidMatch[1], 10), 0);
88
+ return true;
89
+ }
90
+ } catch { /* pid gone or lock unreadable */ }
91
+ return false;
92
+ }
93
+
94
+ function injectWatcherWarning(projectPath, result) {
95
+ if (!isWatcherRunning(projectPath)) {
96
+ result._warning = 'Watcher is NOT running — auto-backup protection is inactive. Any file changes made without a manual snapshot_now call will NOT be captured. Consider starting the watcher or calling snapshot_now before making changes.';
97
+ }
98
+ return result;
99
+ }
100
+
101
+ // ── Server ──────────────────────────────────────────────────────
102
+
103
+ const server = new McpServer({
104
+ name: 'cursor-guard',
105
+ version: pkg.version,
106
+ });
107
+
108
+ // ── Tool 1: doctor ──────────────────────────────────────────────
109
+
110
+ server.tool(
111
+ 'doctor',
112
+ 'Run health checks on a project: environment, config, Git, backup refs, shadow copies, disk space. Read-only, safe to call anytime.',
113
+ {
114
+ path: z.string().describe('Absolute path to the project directory'),
115
+ },
116
+ async ({ path: projectPath }) => {
117
+ const resolved = path.resolve(projectPath);
118
+ ensureWatcher(resolved);
119
+ const result = injectPreWarning(resolved, injectAlert(resolved, runDiagnostics(resolved)));
120
+ injectWatcherWarning(resolved, result);
121
+ return {
122
+ content: [{
123
+ type: 'text',
124
+ text: JSON.stringify(result, null, 2),
125
+ }],
126
+ };
127
+ }
128
+ );
129
+
130
+ // ── Tool 2: list_backups ────────────────────────────────────────
131
+
132
+ server.tool(
133
+ 'list_backups',
134
+ 'List available backup/restore points from all sources (git refs, shadow copies). Read-only. Use before restore to find candidate versions.',
135
+ {
136
+ path: z.string().describe('Absolute path to the project directory'),
137
+ file: z.string().optional().describe('Filter to a specific file (relative path)'),
138
+ before: z.string().optional().describe('Only show backups before this time (e.g. "10 minutes ago", "2026-03-21T14:00:00")'),
139
+ limit: z.number().optional().describe('Max results per source (default 20)'),
140
+ },
141
+ async ({ path: projectPath, file, before, limit }) => {
142
+ const resolved = path.resolve(projectPath);
143
+ ensureWatcher(resolved);
144
+ const result = injectPreWarning(resolved, injectAlert(resolved, listBackups(resolved, { file, before, limit })));
145
+ return {
146
+ content: [{
147
+ type: 'text',
148
+ text: JSON.stringify(result, null, 2),
149
+ }],
150
+ };
151
+ }
152
+ );
153
+
154
+ // ── Tool 3: snapshot_now ────────────────────────────────────────
155
+
156
+ server.tool(
157
+ 'snapshot_now',
158
+ 'Create an immediate backup snapshot of the current project state. Use before risky operations to preserve a restore point. If the snapshot tree matches the previous Guard baseline (no file changes), a bookmark commit is still created on refs/guard/snapshot so intent/time stay visible — not silently skipped.',
159
+ {
160
+ path: z.string().describe('Absolute path to the project directory'),
161
+ strategy: z.enum(['git', 'shadow', 'both']).optional().describe('Backup strategy (default: from config, or "git")'),
162
+ message: z.string().optional().describe('Custom commit message for git snapshot'),
163
+ 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'),
164
+ intent: z.string().optional().describe('Why this snapshot is being created — describe the operation about to happen (e.g. "refactoring auth middleware to use JWT")'),
165
+ agent: z.string().optional().describe('AI model identifier (e.g. "claude-4-opus")'),
166
+ session: z.string().optional().describe('Conversation or session ID for audit trail'),
167
+ },
168
+ async ({ path: projectPath, strategy, message, scope, intent, agent, session }) => {
169
+ const resolved = path.resolve(projectPath);
170
+ ensureWatcher(resolved);
171
+ const { cfg } = loadConfig(resolved);
172
+
173
+ if (scope === 'all') {
174
+ cfg.protect = [];
175
+ } else if (scope === 'protected' && cfg.protect.length === 0) {
176
+ // "protected" requested but no protect patterns configured — snapshot all
177
+ // (no way to filter without patterns)
178
+ }
179
+
180
+ const effectiveStrategy = strategy || cfg.backup_strategy || 'git';
181
+ const results = {};
182
+
183
+ if (effectiveStrategy === 'git' || effectiveStrategy === 'both') {
184
+ const context = { trigger: 'manual' };
185
+ if (intent) context.intent = intent;
186
+ if (agent) context.agent = agent;
187
+ if (session) context.session = session;
188
+ results.git = createGitSnapshot(resolved, cfg, {
189
+ branchRef: 'refs/guard/snapshot',
190
+ message: message || `guard: manual snapshot ${new Date().toISOString()}`,
191
+ context,
192
+ allowEmptyTree: true,
193
+ });
194
+ }
195
+
196
+ if (effectiveStrategy === 'shadow' || effectiveStrategy === 'both') {
197
+ results.shadow = createShadowCopy(resolved, cfg);
198
+ }
199
+
200
+ injectPreWarning(resolved, injectAlert(resolved, results));
201
+ injectWatcherWarning(resolved, results);
202
+
203
+ return {
204
+ content: [{
205
+ type: 'text',
206
+ text: JSON.stringify(results, null, 2),
207
+ }],
208
+ };
209
+ }
210
+ );
211
+
212
+ // ── Tool 3b: record_guard_event (MCP audit bookmark) ─────────────
213
+
214
+ server.tool(
215
+ 'record_guard_event',
216
+ 'Create a Git bookmark on refs/guard/snapshot focused on MCP/agent audit: stores Guard-Event (what happened), optional detail/summary, intent, agent, session. When the tree matches the previous baseline, still creates a commit (same as snapshot_now) so the timeline shows the event with no silent skip. Use after other MCP calls when you need a visible record without relying on file diffs alone.',
217
+ {
218
+ path: z.string().describe('Absolute path to the project directory'),
219
+ event: z.string().describe('Short event label, e.g. restore_project:execute, doctor_fix, list_backups:query'),
220
+ detail: z.string().optional().describe('Longer text stored in Summary trailer (optional)'),
221
+ intent: z.string().optional().describe('Human-readable intent; defaults to event if omitted'),
222
+ agent: z.string().optional().describe('AI model identifier'),
223
+ session: z.string().optional().describe('Conversation or session ID'),
224
+ },
225
+ async ({ path: projectPath, event, detail, intent, agent, session }) => {
226
+ const resolved = path.resolve(projectPath);
227
+ ensureWatcher(resolved);
228
+ const { cfg } = loadConfig(resolved);
229
+
230
+ const ev = trailerScalar(event);
231
+ if (!ev) {
232
+ const err = { git: { status: 'error', error: 'event must be a non-empty string' } };
233
+ injectPreWarning(resolved, injectAlert(resolved, err));
234
+ injectWatcherWarning(resolved, err);
235
+ return { content: [{ type: 'text', text: JSON.stringify(err, null, 2) }] };
236
+ }
237
+
238
+ const context = {
239
+ trigger: 'mcp-event',
240
+ guardEvent: ev,
241
+ summary: detail ? trailerScalar(detail, 2000) : `MCP event: ${ev}`,
242
+ };
243
+ if (intent) context.intent = trailerScalar(intent);
244
+ if (agent) context.agent = trailerScalar(agent);
245
+ if (session) context.session = trailerScalar(session);
246
+
247
+ const results = {
248
+ git: createGitSnapshot(resolved, cfg, {
249
+ branchRef: 'refs/guard/snapshot',
250
+ message: `guard: MCP event ${new Date().toISOString()}`,
251
+ context,
252
+ allowEmptyTree: true,
253
+ fullWorkspaceSnapshot: true,
254
+ }),
255
+ };
256
+
257
+ injectPreWarning(resolved, injectAlert(resolved, results));
258
+ injectWatcherWarning(resolved, results);
259
+
260
+ return {
261
+ content: [{
262
+ type: 'text',
263
+ text: JSON.stringify(results, null, 2),
264
+ }],
265
+ };
266
+ }
267
+ );
268
+
269
+ // ── Tool 4: restore_file ────────────────────────────────────────
270
+
271
+ server.tool(
272
+ 'restore_file',
273
+ 'Restore a single file from a backup source (git commit/ref or shadow copy timestamp). By default, preserves the current version in a pre-restore snapshot before restoring.',
274
+ {
275
+ path: z.string().describe('Absolute path to the project directory'),
276
+ file: z.string().describe('Relative path to the file to restore'),
277
+ source: z.string().describe('Backup source: git commit hash, ref name, or shadow copy timestamp (e.g. "20260321_143205")'),
278
+ preserve_current: z.boolean().optional().describe('Create pre-restore snapshot before restoring (default true)'),
279
+ },
280
+ async ({ path: projectPath, file, source, preserve_current }) => {
281
+ const resolved = path.resolve(projectPath);
282
+ ensureWatcher(resolved);
283
+ const result = injectPreWarning(resolved, injectAlert(resolved, restoreFile(resolved, file, source, {
284
+ preserveCurrent: preserve_current,
285
+ })));
286
+ injectWatcherWarning(resolved, result);
287
+ return {
288
+ content: [{
289
+ type: 'text',
290
+ text: JSON.stringify(result, null, 2),
291
+ }],
292
+ };
293
+ }
294
+ );
295
+
296
+ // ── Tool 5: restore_project ─────────────────────────────────────
297
+
298
+ server.tool(
299
+ 'restore_project',
300
+ 'Preview or execute a full project restore to a given backup point. In preview mode (default), shows affected files (including untracked) without changes. In execute mode, creates a pre-restore snapshot then restores all tracked files and cleans untracked files.',
301
+ {
302
+ path: z.string().describe('Absolute path to the project directory'),
303
+ source: z.string().describe('Backup source: git commit hash or ref name'),
304
+ preview: z.boolean().optional().describe('If true (default), only show what would change. If false, execute the restore.'),
305
+ preserve_current: z.boolean().optional().describe('Create pre-restore snapshot before executing (default true, only used when preview=false)'),
306
+ clean_untracked: z.boolean().optional().describe('Remove untracked non-ignored files after restore (default true, only used when preview=false)'),
307
+ },
308
+ async ({ path: projectPath, source, preview, preserve_current, clean_untracked }) => {
309
+ const resolved = path.resolve(projectPath);
310
+ ensureWatcher(resolved);
311
+
312
+ if (preview !== false) {
313
+ const result = injectPreWarning(resolved, injectAlert(resolved, previewProjectRestore(resolved, source)));
314
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
315
+ }
316
+
317
+ const result = injectPreWarning(resolved, injectAlert(resolved, executeProjectRestore(resolved, source, {
318
+ preserveCurrent: preserve_current,
319
+ cleanUntracked: clean_untracked,
320
+ })));
321
+ injectWatcherWarning(resolved, result);
322
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
323
+ }
324
+ );
325
+
326
+ // ── Tool 6: doctor_fix ──────────────────────────────────────────
327
+
328
+ server.tool(
329
+ 'doctor_fix',
330
+ 'Auto-fix common configuration and environment issues: create missing config, init git repo, add .cursor-guard-backup/ to .gitignore, remove stale lock files, fix strategy mismatch. Each fix is idempotent. Use dry_run=true to preview without changes.',
331
+ {
332
+ path: z.string().describe('Absolute path to the project directory'),
333
+ dry_run: z.boolean().optional().describe('If true, report what would be fixed without modifying anything (default false)'),
334
+ },
335
+ async ({ path: projectPath, dry_run }) => {
336
+ const resolved = path.resolve(projectPath);
337
+ ensureWatcher(resolved);
338
+ const result = injectPreWarning(resolved, injectAlert(resolved, runFixes(resolved, { dryRun: !!dry_run })));
339
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
340
+ }
341
+ );
342
+
343
+ // ── Tool 7: backup_status ───────────────────────────────────────
344
+
345
+ server.tool(
346
+ 'backup_status',
347
+ 'Get comprehensive backup system status: watcher running/stale, last backup time per strategy, configured strategy and retention, guard ref counts, disk space. Read-only, safe to call anytime.',
348
+ {
349
+ path: z.string().describe('Absolute path to the project directory'),
350
+ },
351
+ async ({ path: projectPath }) => {
352
+ const resolved = path.resolve(projectPath);
353
+ ensureWatcher(resolved);
354
+ const result = injectPreWarning(resolved, injectAlert(resolved, getBackupStatus(resolved)));
355
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
356
+ }
357
+ );
358
+
359
+ // ── Tool 8: dashboard ───────────────────────────────────────────
360
+
361
+ server.tool(
362
+ 'dashboard',
363
+ 'Get a comprehensive backup health dashboard: strategy, last backup time, backup counts, disk usage breakdown, protection scope, health assessment, and active alerts. Combines status + analytics in one call.',
364
+ {
365
+ path: z.string().describe('Absolute path to the project directory'),
366
+ },
367
+ async ({ path: projectPath }) => {
368
+ const resolved = path.resolve(projectPath);
369
+ ensureWatcher(resolved);
370
+ const result = injectPreWarning(resolved, injectAlert(resolved, getDashboard(resolved)));
371
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
372
+ }
373
+ );
374
+
375
+ // ── Tool 9: alert_status ────────────────────────────────────────
376
+
377
+ server.tool(
378
+ 'alert_status',
379
+ 'Check if there is an active change-velocity alert (V4 proactive detection). Returns the alert details if active, or confirms no alert. Read-only, safe to call anytime.',
380
+ {
381
+ path: z.string().describe('Absolute path to the project directory'),
382
+ },
383
+ async ({ path: projectPath }) => {
384
+ const resolved = path.resolve(projectPath);
385
+ ensureWatcher(resolved);
386
+ const alert = loadActiveAlert(resolved);
387
+ const result = alert
388
+ ? { active: true, alert }
389
+ : { active: false, message: 'No active alerts' };
390
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
391
+ }
392
+ );
393
+
394
+ // ── Start ───────────────────────────────────────────────────────
395
+
396
+ async function main() {
397
+ const transport = new StdioServerTransport();
398
+ await server.connect(transport);
399
+ }
400
+
401
+ main().catch((err) => {
402
+ console.error('cursor-guard MCP server failed to start:', err);
403
+ process.exit(1);
404
+ });