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,8 +1,23 @@
1
1
  'use strict';
2
2
 
3
- const vscode = require('vscode');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { guardPath } = require('./paths');
4
6
 
5
- const HEARTBEAT_INTERVAL = 30000;
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(), HEARTBEAT_INTERVAL);
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) return;
158
+ if (this._pollRunning) {
159
+ this._pollAgainAfter = true;
160
+ return;
161
+ }
41
162
  this._pollRunning = true;
42
163
  try {
43
- if (!this._dashMgr.running) return;
44
- const projects = await this._dashMgr.getProjects();
45
- if (!Array.isArray(projects)) return;
46
- for (const p of projects) {
47
- const fullData = await this._dashMgr.getFullPageData(p.id);
48
- this._data.set(p.id, {
49
- ...p,
50
- dashboard: fullData?.dashboard || null,
51
- backups: fullData?.backups || [],
52
- scope: fullData?.scope || null,
53
- doctor: fullData?.doctor || null,
54
- });
55
- }
56
- this._emit();
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 { this._pollRunning = false; }
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
- await this._poll();
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.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 = git(["rev-parse", "--verify", branchRef], { cwd, allowFail: true });
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", branchRef], { cwd, env, stdio: "pipe" });
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", `${branchRef}^{tree}`], { cwd, allowFail: true }) : null;
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
- const msg = buildCommitMessage(ts, opts);
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
- result[def.key] = def.parse ? def.parse(m[2]) : m[2];
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 snapshotHash = git(["rev-parse", "--verify", "refs/guard/snapshot"], { cwd: projectDir, allowFail: true });
36442
- if (snapshotHash) {
36443
- const snapLog = git(["log", "-1", "--format=%aI%B", "refs/guard/snapshot"], { cwd: projectDir, allowFail: true });
36444
- const snapParts = snapLog ? snapLog.split("") : [];
36445
- const ts = snapParts[0] || null;
36446
- const snapBody = snapParts[1] || "";
36447
- const snapTrailers = parseCommitTrailers(snapBody);
36448
- const snapSubject = snapBody.split("\n")[0] || "";
36449
- const include = !beforeDate || ts && Date.parse(ts) <= beforeDate.getTime();
36450
- if (include) {
36451
- sources.push({
36452
- type: "git-snapshot",
36453
- ref: "refs/guard/snapshot",
36454
- commitHash: snapshotHash,
36455
- shortHash: snapshotHash.substring(0, 7),
36456
- timestamp: ts,
36457
- message: snapSubject || void 0,
36458
- ...snapTrailers
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.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",