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.
- package/README.md +697 -697
- package/README.zh-CN.md +696 -696
- package/ROADMAP.md +1775 -1720
- package/SKILL.md +631 -629
- package/docs/RELEASE.md +197 -196
- package/docs/SNAPSHOT-BOOKMARK.md +47 -0
- package/package.json +70 -69
- package/references/dashboard/public/app.js +2079 -1832
- package/references/dashboard/public/style.css +1660 -1573
- package/references/dashboard/server.js +197 -4
- package/references/lib/core/backups.js +509 -492
- package/references/lib/core/core.test.js +1761 -1616
- package/references/lib/core/snapshot.js +441 -369
- package/references/mcp/mcp.test.js +381 -362
- package/references/mcp/server.js +404 -347
- package/references/vscode-extension/dist/{cursor-guard-ide-4.9.9.vsix → cursor-guard-ide-4.9.15.vsix} +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +2079 -1832
- package/references/vscode-extension/dist/dashboard/public/style.css +1660 -1573
- package/references/vscode-extension/dist/dashboard/server.js +197 -4
- package/references/vscode-extension/dist/extension.js +780 -704
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/auto-setup.js +201 -192
- package/references/vscode-extension/dist/lib/core/backups.js +509 -492
- package/references/vscode-extension/dist/lib/core/snapshot.js +441 -369
- package/references/vscode-extension/dist/lib/poller.js +161 -21
- package/references/vscode-extension/dist/lib/sidebar-webview.js +22 -0
- package/references/vscode-extension/dist/mcp/server.js +152 -35
- package/references/vscode-extension/dist/package.json +7 -1
- package/references/vscode-extension/dist/skill/ROADMAP.md +1775 -1720
- package/references/vscode-extension/dist/skill/SKILL.md +631 -629
- package/references/vscode-extension/extension.js +780 -704
- package/references/vscode-extension/lib/auto-setup.js +201 -192
- package/references/vscode-extension/lib/poller.js +161 -21
- package/references/vscode-extension/lib/sidebar-webview.js +22 -0
- package/references/vscode-extension/package.json +146 -140
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { guardPath } = require('./paths');
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
/** Safety net if fs.watch misses (platform limits); primary updates are filesystem events. */
|
|
8
|
+
const FALLBACK_POLL_MS = 180000;
|
|
9
|
+
|
|
10
|
+
function tryWatchDirRecursive(dir, cb) {
|
|
11
|
+
try {
|
|
12
|
+
return fs.watch(dir, { recursive: true }, cb);
|
|
13
|
+
} catch {
|
|
14
|
+
try {
|
|
15
|
+
return fs.watch(dir, cb);
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
6
21
|
|
|
7
22
|
class Poller {
|
|
8
23
|
constructor(dashMgr) {
|
|
@@ -11,6 +26,12 @@ class Poller {
|
|
|
11
26
|
this._listeners = [];
|
|
12
27
|
this._data = new Map();
|
|
13
28
|
this._pollRunning = false;
|
|
29
|
+
this._pollAgainAfter = false;
|
|
30
|
+
this._pollWaiters = [];
|
|
31
|
+
this._fsWatchers = [];
|
|
32
|
+
this._watchedRegistryKey = '';
|
|
33
|
+
this._fsDebounceTimer = null;
|
|
34
|
+
this._reattachTimer = null;
|
|
14
35
|
}
|
|
15
36
|
|
|
16
37
|
get data() { return this._data; }
|
|
@@ -26,10 +47,107 @@ class Poller {
|
|
|
26
47
|
}
|
|
27
48
|
}
|
|
28
49
|
|
|
50
|
+
_teardownFsWatchers() {
|
|
51
|
+
if (this._reattachTimer) {
|
|
52
|
+
clearTimeout(this._reattachTimer);
|
|
53
|
+
this._reattachTimer = null;
|
|
54
|
+
}
|
|
55
|
+
if (this._fsDebounceTimer) {
|
|
56
|
+
clearTimeout(this._fsDebounceTimer);
|
|
57
|
+
this._fsDebounceTimer = null;
|
|
58
|
+
}
|
|
59
|
+
for (const w of this._fsWatchers) {
|
|
60
|
+
try { w.close(); } catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
this._fsWatchers = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_scheduleFsRefresh() {
|
|
66
|
+
if (this._fsDebounceTimer) clearTimeout(this._fsDebounceTimer);
|
|
67
|
+
this._fsDebounceTimer = setTimeout(() => {
|
|
68
|
+
this._fsDebounceTimer = null;
|
|
69
|
+
void this.forceRefresh();
|
|
70
|
+
}, 400);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_syncFileWatchers() {
|
|
74
|
+
const reg = this._dashMgr?.registry;
|
|
75
|
+
if (!reg) return;
|
|
76
|
+
const key = [...reg.entries()].map(([k, v]) => `${k}:${v._path}`).sort().join('|');
|
|
77
|
+
if (key === this._watchedRegistryKey) return;
|
|
78
|
+
this._watchedRegistryKey = key;
|
|
79
|
+
|
|
80
|
+
this._teardownFsWatchers();
|
|
81
|
+
|
|
82
|
+
let utils;
|
|
83
|
+
try {
|
|
84
|
+
utils = require(guardPath('lib', 'utils'));
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const { isGitRepo, gitDir: getGitDir } = utils;
|
|
89
|
+
|
|
90
|
+
for (const proj of reg.values()) {
|
|
91
|
+
const pp = proj._path;
|
|
92
|
+
const pid = proj.id;
|
|
93
|
+
|
|
94
|
+
if (isGitRepo(pp)) {
|
|
95
|
+
const gDir = getGitDir(pp);
|
|
96
|
+
if (gDir && fs.existsSync(gDir)) {
|
|
97
|
+
try {
|
|
98
|
+
const w = fs.watch(gDir, (ev, fname) => {
|
|
99
|
+
if (fname === 'cursor-guard-alert.json' || fname === 'cursor-guard.lock') {
|
|
100
|
+
this._scheduleFsRefresh();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
this._fsWatchers.push(w);
|
|
104
|
+
} catch { /* ignore */ }
|
|
105
|
+
|
|
106
|
+
const refsDir = path.join(gDir, 'refs');
|
|
107
|
+
if (fs.existsSync(refsDir)) {
|
|
108
|
+
try {
|
|
109
|
+
const w = fs.watch(refsDir, (ev, fname) => {
|
|
110
|
+
if (!fname) return;
|
|
111
|
+
if (fname === 'guard' || fname.startsWith('guard')) {
|
|
112
|
+
this._scheduleFsRefresh();
|
|
113
|
+
if (!this._reattachTimer) {
|
|
114
|
+
this._reattachTimer = setTimeout(() => {
|
|
115
|
+
this._reattachTimer = null;
|
|
116
|
+
this._watchedRegistryKey = '';
|
|
117
|
+
this._syncFileWatchers();
|
|
118
|
+
}, 600);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
this._fsWatchers.push(w);
|
|
123
|
+
} catch { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const guardDir = path.join(gDir, 'refs', 'guard');
|
|
127
|
+
if (fs.existsSync(guardDir)) {
|
|
128
|
+
const w = tryWatchDirRecursive(guardDir, () => this._scheduleFsRefresh());
|
|
129
|
+
if (w) this._fsWatchers.push(w);
|
|
130
|
+
const preRestoreDir = path.join(guardDir, 'pre-restore');
|
|
131
|
+
if (fs.existsSync(preRestoreDir)) {
|
|
132
|
+
const w2 = tryWatchDirRecursive(preRestoreDir, () => this._scheduleFsRefresh());
|
|
133
|
+
if (w2) this._fsWatchers.push(w2);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const backupDir = path.join(pp, '.cursor-guard-backup');
|
|
140
|
+
if (fs.existsSync(backupDir)) {
|
|
141
|
+
const w = tryWatchDirRecursive(backupDir, () => this._scheduleFsRefresh());
|
|
142
|
+
if (w) this._fsWatchers.push(w);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
29
147
|
start() {
|
|
30
148
|
if (this._timer) return;
|
|
31
|
-
this._poll();
|
|
32
|
-
this._timer = setInterval(() => this._poll(),
|
|
149
|
+
void this._poll();
|
|
150
|
+
this._timer = setInterval(() => void this._poll(), FALLBACK_POLL_MS);
|
|
33
151
|
}
|
|
34
152
|
|
|
35
153
|
stop() {
|
|
@@ -37,33 +155,55 @@ class Poller {
|
|
|
37
155
|
}
|
|
38
156
|
|
|
39
157
|
async _poll() {
|
|
40
|
-
if (this._pollRunning)
|
|
158
|
+
if (this._pollRunning) {
|
|
159
|
+
this._pollAgainAfter = true;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
41
162
|
this._pollRunning = true;
|
|
42
163
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
164
|
+
do {
|
|
165
|
+
this._pollAgainAfter = false;
|
|
166
|
+
if (!this._dashMgr.running) break;
|
|
167
|
+
const projects = await this._dashMgr.getProjects();
|
|
168
|
+
if (!Array.isArray(projects)) break;
|
|
169
|
+
for (const p of projects) {
|
|
170
|
+
const fullData = await this._dashMgr.getFullPageData(p.id);
|
|
171
|
+
this._data.set(p.id, {
|
|
172
|
+
...p,
|
|
173
|
+
dashboard: fullData?.dashboard || null,
|
|
174
|
+
backups: fullData?.backups || [],
|
|
175
|
+
scope: fullData?.scope || null,
|
|
176
|
+
doctor: fullData?.doctor || null,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
this._emit();
|
|
180
|
+
} while (this._pollAgainAfter);
|
|
57
181
|
} catch { /* non-critical */ }
|
|
58
|
-
finally {
|
|
182
|
+
finally {
|
|
183
|
+
this._pollRunning = false;
|
|
184
|
+
const waiters = this._pollWaiters.splice(0);
|
|
185
|
+
for (const fn of waiters) {
|
|
186
|
+
try { fn(); } catch { /* ignore */ }
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
this._syncFileWatchers();
|
|
190
|
+
} catch { /* ignore */ }
|
|
191
|
+
}
|
|
59
192
|
}
|
|
60
193
|
|
|
61
194
|
async forceRefresh() {
|
|
62
|
-
|
|
195
|
+
return new Promise(resolve => {
|
|
196
|
+
this._pollWaiters.push(resolve);
|
|
197
|
+
this._pollAgainAfter = true;
|
|
198
|
+
if (!this._pollRunning) {
|
|
199
|
+
void this._poll();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
63
202
|
}
|
|
64
203
|
|
|
65
204
|
dispose() {
|
|
66
205
|
this.stop();
|
|
206
|
+
this._teardownFsWatchers();
|
|
67
207
|
this._listeners = [];
|
|
68
208
|
}
|
|
69
209
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const vscode = require('vscode');
|
|
4
4
|
const { getLocale, setLocale } = require('./locale');
|
|
5
|
+
const { guardPath } = require('./paths');
|
|
5
6
|
|
|
6
7
|
class SidebarDashboardProvider {
|
|
7
8
|
constructor(poller, context) {
|
|
@@ -41,6 +42,19 @@ class SidebarDashboardProvider {
|
|
|
41
42
|
this._postLocale();
|
|
42
43
|
}
|
|
43
44
|
if (msg.cmd === 'exec') vscode.commands.executeCommand(msg.command);
|
|
45
|
+
if (msg.cmd === 'clearAlert' && msg.projectId) {
|
|
46
|
+
(async () => {
|
|
47
|
+
try {
|
|
48
|
+
const reg = this._poller._dashMgr?.registry;
|
|
49
|
+
const entry = reg?.get(msg.projectId);
|
|
50
|
+
if (entry?._path) {
|
|
51
|
+
const { clearAlert } = require(guardPath('lib', 'core', 'anomaly'));
|
|
52
|
+
clearAlert(entry._path);
|
|
53
|
+
await this._poller.forceRefresh();
|
|
54
|
+
}
|
|
55
|
+
} catch { /* ignore */ }
|
|
56
|
+
})();
|
|
57
|
+
}
|
|
44
58
|
});
|
|
45
59
|
|
|
46
60
|
webviewView.onDidChangeVisibility(() => {
|
|
@@ -1058,6 +1072,7 @@ const I18N = {
|
|
|
1058
1072
|
'actions.openDashboard': '打开看板',
|
|
1059
1073
|
'actions.restore': '恢复',
|
|
1060
1074
|
'actions.viewDetails': '查看详情',
|
|
1075
|
+
'actions.dismissAlert': '忽略告警',
|
|
1061
1076
|
'actions.snapshot': '立即快照',
|
|
1062
1077
|
'actions.watcherOn': '停止 Watcher',
|
|
1063
1078
|
'actions.watcherOff': '启动 Watcher',
|
|
@@ -1290,6 +1305,13 @@ function render(projects) {
|
|
|
1290
1305
|
vscode.postMessage({ cmd: 'exec', command: btn.dataset.cmd });
|
|
1291
1306
|
});
|
|
1292
1307
|
});
|
|
1308
|
+
|
|
1309
|
+
root.querySelectorAll('[data-dismiss-alert]').forEach(btn => {
|
|
1310
|
+
btn.addEventListener('click', () => {
|
|
1311
|
+
const pid = btn.getAttribute('data-dismiss-alert');
|
|
1312
|
+
if (pid) vscode.postMessage({ cmd: 'clearAlert', projectId: pid });
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1293
1315
|
}
|
|
1294
1316
|
|
|
1295
1317
|
function renderProject(dashboard, projectId) {
|
|
@@ -35594,7 +35594,7 @@ var require_package = __commonJS({
|
|
|
35594
35594
|
"package.json"(exports2, module2) {
|
|
35595
35595
|
module2.exports = {
|
|
35596
35596
|
name: "cursor-guard",
|
|
35597
|
-
version: "4.9.
|
|
35597
|
+
version: "4.9.15",
|
|
35598
35598
|
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",
|
|
35599
35599
|
keywords: [
|
|
35600
35600
|
"cursor",
|
|
@@ -35633,6 +35633,7 @@ var require_package = __commonJS({
|
|
|
35633
35633
|
"README.md",
|
|
35634
35634
|
"README.zh-CN.md",
|
|
35635
35635
|
"docs/RELEASE.md",
|
|
35636
|
+
"docs/SNAPSHOT-BOOKMARK.md",
|
|
35636
35637
|
"ROADMAP.md",
|
|
35637
35638
|
"LICENSE",
|
|
35638
35639
|
"references/auto-backup.ps1",
|
|
@@ -36014,6 +36015,25 @@ var require_snapshot = __commonJS({
|
|
|
36014
36015
|
const pad = (n) => String(n).padStart(2, "0");
|
|
36015
36016
|
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
36016
36017
|
}
|
|
36018
|
+
var REF_GUARD_AUTO_BACKUP = "refs/guard/auto-backup";
|
|
36019
|
+
var REF_GUARD_SNAPSHOT = "refs/guard/snapshot";
|
|
36020
|
+
function resolveGuardParentHash(cwd, branchRef) {
|
|
36021
|
+
if (branchRef !== REF_GUARD_AUTO_BACKUP && branchRef !== REF_GUARD_SNAPSHOT) {
|
|
36022
|
+
return git(["rev-parse", "--verify", branchRef], { cwd, allowFail: true });
|
|
36023
|
+
}
|
|
36024
|
+
const autoH = git(["rev-parse", "--verify", REF_GUARD_AUTO_BACKUP], { cwd, allowFail: true });
|
|
36025
|
+
const snapH = git(["rev-parse", "--verify", REF_GUARD_SNAPSHOT], { cwd, allowFail: true });
|
|
36026
|
+
if (!autoH && !snapH) return null;
|
|
36027
|
+
if (!autoH) return snapH;
|
|
36028
|
+
if (!snapH) return autoH;
|
|
36029
|
+
const commitUnix = (h) => {
|
|
36030
|
+
const s = git(["log", "-1", "--format=%ct", h], { cwd, allowFail: true });
|
|
36031
|
+
return s ? parseInt(String(s).trim(), 10) : 0;
|
|
36032
|
+
};
|
|
36033
|
+
const tAuto = commitUnix(autoH);
|
|
36034
|
+
const tSnap = commitUnix(snapH);
|
|
36035
|
+
return tSnap > tAuto ? snapH : autoH;
|
|
36036
|
+
}
|
|
36017
36037
|
function listIndexFiles(cwd, env) {
|
|
36018
36038
|
try {
|
|
36019
36039
|
const out = execFileSync("git", ["ls-files", "--cached"], {
|
|
@@ -36059,6 +36079,10 @@ var require_snapshot = __commonJS({
|
|
|
36059
36079
|
}
|
|
36060
36080
|
return excluded;
|
|
36061
36081
|
}
|
|
36082
|
+
function trailerScalar2(val, maxLen = 500) {
|
|
36083
|
+
if (val == null) return "";
|
|
36084
|
+
return String(val).replace(/\r\n/g, " ").replace(/\r/g, " ").replace(/\n/g, " ").trim().slice(0, maxLen);
|
|
36085
|
+
}
|
|
36062
36086
|
function buildCommitMessage(ts, opts) {
|
|
36063
36087
|
if (opts.message && !opts.context) return opts.message;
|
|
36064
36088
|
const ctx = opts.context || {};
|
|
@@ -36066,11 +36090,12 @@ var require_snapshot = __commonJS({
|
|
|
36066
36090
|
const subject = opts.message || `guard: auto-backup ${ts}${countTag}`;
|
|
36067
36091
|
const trailers = [];
|
|
36068
36092
|
if (ctx.changedFileCount != null) trailers.push(`Files-Changed: ${ctx.changedFileCount}`);
|
|
36069
|
-
if (ctx.summary) trailers.push(`Summary: ${ctx.summary}`);
|
|
36070
|
-
if (ctx.trigger) trailers.push(`Trigger: ${ctx.trigger}`);
|
|
36071
|
-
if (ctx.intent) trailers.push(`Intent: ${ctx.intent}`);
|
|
36072
|
-
if (ctx.agent) trailers.push(`Agent: ${ctx.agent}`);
|
|
36073
|
-
if (ctx.session) trailers.push(`Session: ${ctx.session}`);
|
|
36093
|
+
if (ctx.summary) trailers.push(`Summary: ${trailerScalar2(ctx.summary, 2e3)}`);
|
|
36094
|
+
if (ctx.trigger) trailers.push(`Trigger: ${trailerScalar2(ctx.trigger)}`);
|
|
36095
|
+
if (ctx.intent) trailers.push(`Intent: ${trailerScalar2(ctx.intent)}`);
|
|
36096
|
+
if (ctx.agent) trailers.push(`Agent: ${trailerScalar2(ctx.agent)}`);
|
|
36097
|
+
if (ctx.session) trailers.push(`Session: ${trailerScalar2(ctx.session)}`);
|
|
36098
|
+
if (ctx.guardEvent) trailers.push(`Guard-Event: ${trailerScalar2(ctx.guardEvent)}`);
|
|
36074
36099
|
if (trailers.length === 0) return subject;
|
|
36075
36100
|
return subject + "\n\n" + trailers.join("\n");
|
|
36076
36101
|
}
|
|
@@ -36092,23 +36117,24 @@ var require_snapshot = __commonJS({
|
|
|
36092
36117
|
} catch {
|
|
36093
36118
|
}
|
|
36094
36119
|
try {
|
|
36095
|
-
const parentHash =
|
|
36120
|
+
const parentHash = resolveGuardParentHash(cwd, branchRef);
|
|
36096
36121
|
if (narrowProtect) {
|
|
36097
36122
|
execFileSync("git", ["add", "-A"], { cwd, env, stdio: "pipe" });
|
|
36098
36123
|
pruneIndexFiles(cwd, env, (f) => !matchesAny(cfg.protect, f, { strict: true }));
|
|
36099
36124
|
} else {
|
|
36100
36125
|
if (parentHash) {
|
|
36101
|
-
execFileSync("git", ["read-tree",
|
|
36126
|
+
execFileSync("git", ["read-tree", parentHash], { cwd, env, stdio: "pipe" });
|
|
36102
36127
|
}
|
|
36103
36128
|
execFileSync("git", ["add", "-A"], { cwd, env, stdio: "pipe" });
|
|
36104
36129
|
}
|
|
36105
36130
|
pruneIndexFiles(cwd, env, (f) => matchesAny(cfg.ignore, f));
|
|
36106
36131
|
const secretsExcluded = removeSecretsFromIndex(cfg.secrets_patterns, cwd, env);
|
|
36107
36132
|
const newTree = execFileSync("git", ["write-tree"], { cwd, env, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
36108
|
-
const parentTree = parentHash ? git(["rev-parse", `${
|
|
36133
|
+
const parentTree = parentHash ? git(["rev-parse", `${parentHash}^{tree}`], { cwd, allowFail: true }) : null;
|
|
36109
36134
|
if (newTree === parentTree && !opts.allowEmptyTree) {
|
|
36110
36135
|
return { status: "skipped", reason: "tree unchanged" };
|
|
36111
36136
|
}
|
|
36137
|
+
const isBookmarkCommit = !!(opts.allowEmptyTree && parentTree && newTree === parentTree);
|
|
36112
36138
|
let changedCount;
|
|
36113
36139
|
let incrementalSummary;
|
|
36114
36140
|
let changedFiles;
|
|
@@ -36197,8 +36223,32 @@ var require_snapshot = __commonJS({
|
|
|
36197
36223
|
if (changedCount != null && opts.context) {
|
|
36198
36224
|
opts.context.changedFileCount = changedCount;
|
|
36199
36225
|
}
|
|
36226
|
+
if (isBookmarkCommit && opts.context) {
|
|
36227
|
+
const s = opts.context.summary;
|
|
36228
|
+
if (s == null || String(s).trim() === "") {
|
|
36229
|
+
opts.context.summary = "No file changes since last Guard baseline (bookmark).";
|
|
36230
|
+
}
|
|
36231
|
+
if (opts.context.changedFileCount == null) opts.context.changedFileCount = 0;
|
|
36232
|
+
}
|
|
36200
36233
|
const ts = formatTimestamp(/* @__PURE__ */ new Date());
|
|
36201
|
-
|
|
36234
|
+
let msg = buildCommitMessage(ts, opts);
|
|
36235
|
+
const autoTip = git(["rev-parse", "--verify", REF_GUARD_AUTO_BACKUP], { cwd, allowFail: true });
|
|
36236
|
+
const snapTip = git(["rev-parse", "--verify", REF_GUARD_SNAPSHOT], { cwd, allowFail: true });
|
|
36237
|
+
const autoTipTrim = autoTip ? String(autoTip).trim() : "";
|
|
36238
|
+
const snapTipTrim = snapTip ? String(snapTip).trim() : "";
|
|
36239
|
+
let diffBaseLabel = "initial";
|
|
36240
|
+
if (parentHash) {
|
|
36241
|
+
if (parentHash === autoTipTrim) diffBaseLabel = "auto-backup";
|
|
36242
|
+
else if (parentHash === snapTipTrim) diffBaseLabel = "snapshot";
|
|
36243
|
+
else diffBaseLabel = "other";
|
|
36244
|
+
}
|
|
36245
|
+
const scopeTrailer = narrowProtect ? "narrow" : "full";
|
|
36246
|
+
const guardBlock = `Guard-Diff-Base: ${diffBaseLabel}
|
|
36247
|
+
Guard-Scope: ${scopeTrailer}${isBookmarkCommit ? "\nGuard-Bookmark: true" : ""}`;
|
|
36248
|
+
msg = msg.includes("\n\n") ? `${msg}
|
|
36249
|
+
${guardBlock}` : `${msg}
|
|
36250
|
+
|
|
36251
|
+
${guardBlock}`;
|
|
36202
36252
|
const commitArgs = parentHash ? ["commit-tree", newTree, "-p", parentHash, "-m", msg] : ["commit-tree", newTree, "-m", msg];
|
|
36203
36253
|
const commitHash = execFileSync("git", commitArgs, { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
36204
36254
|
if (!commitHash) {
|
|
@@ -36215,7 +36265,8 @@ var require_snapshot = __commonJS({
|
|
|
36215
36265
|
changedCount,
|
|
36216
36266
|
changedFiles,
|
|
36217
36267
|
incrementalSummary,
|
|
36218
|
-
secretsExcluded: secretsExcluded.length > 0 ? secretsExcluded : void 0
|
|
36268
|
+
secretsExcluded: secretsExcluded.length > 0 ? secretsExcluded : void 0,
|
|
36269
|
+
...isBookmarkCommit ? { bookmark: true } : {}
|
|
36219
36270
|
};
|
|
36220
36271
|
} catch (e) {
|
|
36221
36272
|
return { status: "error", error: e.message };
|
|
@@ -36299,7 +36350,7 @@ var require_snapshot = __commonJS({
|
|
|
36299
36350
|
return { status: "error", error: e.message };
|
|
36300
36351
|
}
|
|
36301
36352
|
}
|
|
36302
|
-
module2.exports = { createGitSnapshot: createGitSnapshot2, createShadowCopy: createShadowCopy2, formatTimestamp, removeSecretsFromIndex };
|
|
36353
|
+
module2.exports = { createGitSnapshot: createGitSnapshot2, createShadowCopy: createShadowCopy2, formatTimestamp, removeSecretsFromIndex, trailerScalar: trailerScalar2 };
|
|
36303
36354
|
}
|
|
36304
36355
|
});
|
|
36305
36356
|
|
|
@@ -36351,9 +36402,13 @@ var require_backups = __commonJS({
|
|
|
36351
36402
|
"Intent": { key: "intent" },
|
|
36352
36403
|
"Agent": { key: "agent" },
|
|
36353
36404
|
"Session": { key: "session" },
|
|
36405
|
+
"Guard-Event": { key: "guardEvent" },
|
|
36354
36406
|
"From": { key: "from" },
|
|
36355
36407
|
"Restore-To": { key: "restoreTo" },
|
|
36356
|
-
"File": { key: "restoreFile" }
|
|
36408
|
+
"File": { key: "restoreFile" },
|
|
36409
|
+
"Guard-Diff-Base": { key: "guardDiffBase" },
|
|
36410
|
+
"Guard-Scope": { key: "guardScope" },
|
|
36411
|
+
"Guard-Bookmark": { key: "guardBookmark", parse: (v) => String(v).trim().toLowerCase() === "true" }
|
|
36357
36412
|
};
|
|
36358
36413
|
function parseCommitTrailers(body) {
|
|
36359
36414
|
if (!body) return {};
|
|
@@ -36363,7 +36418,9 @@ var require_backups = __commonJS({
|
|
|
36363
36418
|
const m = line.match(pattern);
|
|
36364
36419
|
if (m) {
|
|
36365
36420
|
const def = TRAILER_MAP[m[1]];
|
|
36366
|
-
|
|
36421
|
+
const raw = m[2].replace(/\r/g, "");
|
|
36422
|
+
const val = def.parse ? def.parse(raw) : raw.trim();
|
|
36423
|
+
result[def.key] = typeof val === "string" ? val.trim() : val;
|
|
36367
36424
|
}
|
|
36368
36425
|
}
|
|
36369
36426
|
return result;
|
|
@@ -36438,25 +36495,36 @@ var require_backups = __commonJS({
|
|
|
36438
36495
|
sources.push(entry);
|
|
36439
36496
|
}
|
|
36440
36497
|
}
|
|
36441
|
-
const
|
|
36442
|
-
|
|
36443
|
-
|
|
36444
|
-
const
|
|
36445
|
-
|
|
36446
|
-
|
|
36447
|
-
const
|
|
36448
|
-
|
|
36449
|
-
|
|
36450
|
-
|
|
36451
|
-
|
|
36452
|
-
|
|
36453
|
-
|
|
36454
|
-
|
|
36455
|
-
|
|
36456
|
-
|
|
36457
|
-
|
|
36458
|
-
|
|
36459
|
-
|
|
36498
|
+
const snapRef = "refs/guard/snapshot";
|
|
36499
|
+
const snapshotExists = git(["rev-parse", "--verify", snapRef], { cwd: projectDir, allowFail: true });
|
|
36500
|
+
if (snapshotExists) {
|
|
36501
|
+
const snapLogArgs = ["log", snapRef, "--format=%H%aI%B", `-${limit}`];
|
|
36502
|
+
if (opts.before) snapLogArgs.push(`--before=${opts.before}`);
|
|
36503
|
+
if (opts.file) snapLogArgs.push("--", opts.file);
|
|
36504
|
+
const snapOut = git(snapLogArgs, { cwd: projectDir, allowFail: true });
|
|
36505
|
+
if (snapOut) {
|
|
36506
|
+
for (const record of snapOut.split("").filter((r) => r.trim())) {
|
|
36507
|
+
const parts = record.split("");
|
|
36508
|
+
if (parts.length < 3) continue;
|
|
36509
|
+
const hash = parts[0].trim();
|
|
36510
|
+
const timestamp = parts[1];
|
|
36511
|
+
const body = parts[2];
|
|
36512
|
+
const subject = body.split("\n")[0];
|
|
36513
|
+
const trailers = parseCommitTrailers(body);
|
|
36514
|
+
if (beforeDate && timestamp) {
|
|
36515
|
+
const ms = Date.parse(timestamp);
|
|
36516
|
+
if (!isNaN(ms) && ms > beforeDate.getTime()) continue;
|
|
36517
|
+
}
|
|
36518
|
+
sources.push({
|
|
36519
|
+
type: "git-snapshot",
|
|
36520
|
+
ref: snapRef,
|
|
36521
|
+
commitHash: hash,
|
|
36522
|
+
shortHash: hash.substring(0, 7),
|
|
36523
|
+
timestamp,
|
|
36524
|
+
message: subject || void 0,
|
|
36525
|
+
...trailers
|
|
36526
|
+
});
|
|
36527
|
+
}
|
|
36460
36528
|
}
|
|
36461
36529
|
}
|
|
36462
36530
|
}
|
|
@@ -38050,7 +38118,7 @@ var { McpServer } = require_mcp();
|
|
|
38050
38118
|
var { StdioServerTransport } = require_stdio2();
|
|
38051
38119
|
var { z } = require_zod();
|
|
38052
38120
|
var { runDiagnostics } = require_doctor();
|
|
38053
|
-
var { createGitSnapshot, createShadowCopy } = require_snapshot();
|
|
38121
|
+
var { createGitSnapshot, createShadowCopy, trailerScalar } = require_snapshot();
|
|
38054
38122
|
var { listBackups } = require_backups();
|
|
38055
38123
|
var { restoreFile, previewProjectRestore, executeProjectRestore } = require_restore();
|
|
38056
38124
|
var { runFixes } = require_doctor_fix();
|
|
@@ -38172,7 +38240,7 @@ server.tool(
|
|
|
38172
38240
|
);
|
|
38173
38241
|
server.tool(
|
|
38174
38242
|
"snapshot_now",
|
|
38175
|
-
"Create an immediate backup snapshot of the current project state. Use before risky operations to preserve a restore point.",
|
|
38243
|
+
"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 \u2014 not silently skipped.",
|
|
38176
38244
|
{
|
|
38177
38245
|
path: z.string().describe("Absolute path to the project directory"),
|
|
38178
38246
|
strategy: z.enum(["git", "shadow", "both"]).optional().describe('Backup strategy (default: from config, or "git")'),
|
|
@@ -38217,6 +38285,55 @@ server.tool(
|
|
|
38217
38285
|
};
|
|
38218
38286
|
}
|
|
38219
38287
|
);
|
|
38288
|
+
server.tool(
|
|
38289
|
+
"record_guard_event",
|
|
38290
|
+
"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.",
|
|
38291
|
+
{
|
|
38292
|
+
path: z.string().describe("Absolute path to the project directory"),
|
|
38293
|
+
event: z.string().describe("Short event label, e.g. restore_project:execute, doctor_fix, list_backups:query"),
|
|
38294
|
+
detail: z.string().optional().describe("Longer text stored in Summary trailer (optional)"),
|
|
38295
|
+
intent: z.string().optional().describe("Human-readable intent; defaults to event if omitted"),
|
|
38296
|
+
agent: z.string().optional().describe("AI model identifier"),
|
|
38297
|
+
session: z.string().optional().describe("Conversation or session ID")
|
|
38298
|
+
},
|
|
38299
|
+
async ({ path: projectPath, event, detail, intent, agent, session }) => {
|
|
38300
|
+
const resolved = path.resolve(projectPath);
|
|
38301
|
+
ensureWatcher(resolved);
|
|
38302
|
+
const { cfg } = loadConfig(resolved);
|
|
38303
|
+
const ev = trailerScalar(event);
|
|
38304
|
+
if (!ev) {
|
|
38305
|
+
const err = { git: { status: "error", error: "event must be a non-empty string" } };
|
|
38306
|
+
injectPreWarning(resolved, injectAlert(resolved, err));
|
|
38307
|
+
injectWatcherWarning(resolved, err);
|
|
38308
|
+
return { content: [{ type: "text", text: JSON.stringify(err, null, 2) }] };
|
|
38309
|
+
}
|
|
38310
|
+
const context = {
|
|
38311
|
+
trigger: "mcp-event",
|
|
38312
|
+
guardEvent: ev,
|
|
38313
|
+
summary: detail ? trailerScalar(detail, 2e3) : `MCP event: ${ev}`
|
|
38314
|
+
};
|
|
38315
|
+
if (intent) context.intent = trailerScalar(intent);
|
|
38316
|
+
if (agent) context.agent = trailerScalar(agent);
|
|
38317
|
+
if (session) context.session = trailerScalar(session);
|
|
38318
|
+
const results = {
|
|
38319
|
+
git: createGitSnapshot(resolved, cfg, {
|
|
38320
|
+
branchRef: "refs/guard/snapshot",
|
|
38321
|
+
message: `guard: MCP event ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
38322
|
+
context,
|
|
38323
|
+
allowEmptyTree: true,
|
|
38324
|
+
fullWorkspaceSnapshot: true
|
|
38325
|
+
})
|
|
38326
|
+
};
|
|
38327
|
+
injectPreWarning(resolved, injectAlert(resolved, results));
|
|
38328
|
+
injectWatcherWarning(resolved, results);
|
|
38329
|
+
return {
|
|
38330
|
+
content: [{
|
|
38331
|
+
type: "text",
|
|
38332
|
+
text: JSON.stringify(results, null, 2)
|
|
38333
|
+
}]
|
|
38334
|
+
};
|
|
38335
|
+
}
|
|
38336
|
+
);
|
|
38220
38337
|
server.tool(
|
|
38221
38338
|
"restore_file",
|
|
38222
38339
|
"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.",
|
|
@@ -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.9.
|
|
5
|
+
"version": "4.9.15",
|
|
6
6
|
"publisher": "zhangqiang8vipp",
|
|
7
7
|
"license": "BUSL-1.1",
|
|
8
8
|
"engines": {
|
|
@@ -67,6 +67,12 @@
|
|
|
67
67
|
"category": "Cursor Guard",
|
|
68
68
|
"icon": "$(search)"
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
"command": "cursorGuard.setupSkill",
|
|
72
|
+
"title": "Install Agent Skill",
|
|
73
|
+
"category": "Cursor Guard",
|
|
74
|
+
"icon": "$(book)"
|
|
75
|
+
},
|
|
70
76
|
{
|
|
71
77
|
"command": "cursorGuard.addToProtect",
|
|
72
78
|
"title": "Add to Protected",
|