cursor-guard 4.3.2 → 4.3.3
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 +7 -0
- package/README.zh-CN.md +7 -0
- package/SKILL.md +6 -2
- package/package.json +1 -1
- package/references/dashboard/public/app.js +13 -1
- package/references/dashboard/public/style.css +3 -1
- package/references/lib/core/backups.js +13 -4
- package/references/lib/core/snapshot.js +7 -1
- package/references/mcp/server.js +9 -2
package/README.md
CHANGED
|
@@ -377,6 +377,13 @@ The skill activates on these signals:
|
|
|
377
377
|
|
|
378
378
|
## Changelog
|
|
379
379
|
|
|
380
|
+
### v4.3.3
|
|
381
|
+
|
|
382
|
+
- **Feature**: Intent context for snapshots — `snapshot_now` now accepts `intent`, `agent`, and `session` parameters, stored as Git commit trailers to form an audit trail per operation
|
|
383
|
+
- **Feature**: Dashboard displays intent badge in backup table and full intent/agent/session fields in restore-point drawer
|
|
384
|
+
- **Improve**: `parseCommitTrailers` refactored to a data-driven map, supporting all 6 trailer fields (Files-Changed, Summary, Trigger, Intent, Agent, Session)
|
|
385
|
+
- **Improve**: SKILL.md updated to guide AI agents to pass `intent` describing the operation about to happen
|
|
386
|
+
|
|
380
387
|
### v4.3.2
|
|
381
388
|
|
|
382
389
|
- **Fix**: `cursor-guard-init` now adds `node_modules/` (root-level) to `.gitignore` — prevents `git add -A` from scanning thousands of npm dependency files after `npm install cursor-guard --save-dev`
|
package/README.zh-CN.md
CHANGED
|
@@ -377,6 +377,13 @@ node references\dashboard\server.js --path "D:\MyProject"
|
|
|
377
377
|
|
|
378
378
|
## 更新日志
|
|
379
379
|
|
|
380
|
+
### v4.3.3
|
|
381
|
+
|
|
382
|
+
- **功能**:快照意图上下文——`snapshot_now` 新增 `intent`(操作意图)、`agent`(AI 模型)、`session`(会话 ID)参数,作为 Git commit trailer 存储,形成按操作事件的审计链
|
|
383
|
+
- **功能**:仪表盘备份表格显示意图徽章,恢复点抽屉完整展示 intent/agent/session 字段
|
|
384
|
+
- **改进**:`parseCommitTrailers` 重构为数据驱动映射表,支持全部 6 个 trailer 字段
|
|
385
|
+
- **改进**:SKILL.md 更新指引,要求 AI agent 在调用 `snapshot_now` 时传入 `intent` 描述即将执行的操作
|
|
386
|
+
|
|
380
387
|
### v4.3.2
|
|
381
388
|
|
|
382
389
|
- **修复**:`cursor-guard-init` 现在会将根目录 `node_modules/` 加入 `.gitignore`——防止 `npm install cursor-guard --save-dev` 后 `git add -A` 扫描数千个依赖文件导致极度缓慢
|
package/SKILL.md
CHANGED
|
@@ -147,14 +147,18 @@ When the target file of an edit **falls outside the protected scope**, the agent
|
|
|
147
147
|
|
|
148
148
|
> **MCP shortcut**: if `snapshot_now` tool is available, call it with `{ "path": "<project>", "strategy": "git" }` instead of the shell commands below. The tool handles temp index, secrets exclusion, and ref creation internally, and returns `{ "git": { "status": "created", "commitHash": "...", "shortHash": "..." } }`. Report the `shortHash` to the user and proceed.
|
|
149
149
|
>
|
|
150
|
-
> **Best practice —
|
|
150
|
+
> **Best practice — intent context**: Always provide `intent` to describe *what operation you are about to perform and why*. This creates an audit trail so the user can later understand "what was the AI doing when this backup was made". Also pass `message` for the commit subject. Example:
|
|
151
151
|
> ```json
|
|
152
152
|
> {
|
|
153
153
|
> "path": "/project",
|
|
154
154
|
> "strategy": "git",
|
|
155
|
-
> "message": "guard: before refactoring auth middleware
|
|
155
|
+
> "message": "guard: before refactoring auth middleware",
|
|
156
|
+
> "intent": "用户要求将 app.js 中的 session 逻辑拆分到 middleware/auth.js,涉及 3 个函数重写",
|
|
157
|
+
> "agent": "claude-4-opus",
|
|
158
|
+
> "session": "6290c87f"
|
|
156
159
|
> }
|
|
157
160
|
> ```
|
|
161
|
+
> The `intent`, `agent`, and `session` fields are stored as Git commit trailers and displayed in the dashboard restore-point list and detail drawer, forming a complete audit trail per operation.
|
|
158
162
|
|
|
159
163
|
Use a **temporary index and dedicated ref** so the user's staged/unstaged state is never touched:
|
|
160
164
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.3",
|
|
4
4
|
"description": "Protects code from accidental AI overwrite or deletion in Cursor IDE — mandatory pre-write snapshots, review-before-apply, local Git safety net, and deterministic recovery. | 保护代码免受 Cursor AI 代理意外覆写或删除——强制写前快照、预览再执行、本地 Git 安全网、确定性恢复。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cursor",
|
|
@@ -100,6 +100,9 @@ const I18N = {
|
|
|
100
100
|
'summary.deleted': 'Deleted',
|
|
101
101
|
'summary.renamed': 'Renamed',
|
|
102
102
|
'summary.files': 'files',
|
|
103
|
+
'drawer.field.intent': 'Intent',
|
|
104
|
+
'drawer.field.agent': 'Agent',
|
|
105
|
+
'drawer.field.session': 'Session',
|
|
103
106
|
|
|
104
107
|
'error.fetchFailed': 'Failed to fetch data',
|
|
105
108
|
'error.sectionFailed': 'This section failed to load',
|
|
@@ -278,6 +281,9 @@ const I18N = {
|
|
|
278
281
|
'summary.deleted': '删除',
|
|
279
282
|
'summary.renamed': '重命名',
|
|
280
283
|
'summary.files': '个文件',
|
|
284
|
+
'drawer.field.intent': '操作意图',
|
|
285
|
+
'drawer.field.agent': 'AI 模型',
|
|
286
|
+
'drawer.field.session': '会话 ID',
|
|
281
287
|
|
|
282
288
|
'error.fetchFailed': '数据拉取失败',
|
|
283
289
|
'error.sectionFailed': '此区块加载失败',
|
|
@@ -821,7 +827,10 @@ function formatSummaryCell(b) {
|
|
|
821
827
|
const short = translated.length > 80 ? translated.substring(0, 77) + '...' : translated;
|
|
822
828
|
parts.push(`<span class="text-muted text-sm">${esc(short)}</span>`);
|
|
823
829
|
}
|
|
824
|
-
if (b.
|
|
830
|
+
if (b.intent) {
|
|
831
|
+
const intentShort = b.intent.length > 60 ? b.intent.substring(0, 57) + '...' : b.intent;
|
|
832
|
+
parts.push(`<span class="badge badge-intent">${esc(intentShort)}</span>`);
|
|
833
|
+
} else if (b.message && !b.message.startsWith('guard:')) {
|
|
825
834
|
const msgShort = b.message.length > 50 ? b.message.substring(0, 47) + '...' : b.message;
|
|
826
835
|
parts.push(`<span class="text-muted text-sm">${esc(msgShort)}</span>`);
|
|
827
836
|
}
|
|
@@ -952,6 +961,9 @@ function openRestoreDrawer(backup) {
|
|
|
952
961
|
if (backup.ref) fields.push({ key: 'drawer.field.ref', val: backup.ref });
|
|
953
962
|
if (backup.commitHash) fields.push({ key: 'drawer.field.hash', val: backup.commitHash });
|
|
954
963
|
if (backup.path) fields.push({ key: 'drawer.field.path', val: backup.path });
|
|
964
|
+
if (backup.intent) fields.push({ key: 'drawer.field.intent', val: backup.intent });
|
|
965
|
+
if (backup.agent) fields.push({ key: 'drawer.field.agent', val: backup.agent });
|
|
966
|
+
if (backup.session) fields.push({ key: 'drawer.field.session', val: backup.session });
|
|
955
967
|
if (backup.message) fields.push({ key: 'drawer.field.message', val: backup.message });
|
|
956
968
|
if (backup.summary) fields.push({ key: 'drawer.field.summary', val: translateSummary(backup.summary) });
|
|
957
969
|
|
|
@@ -248,9 +248,11 @@ main {
|
|
|
248
248
|
.badge-shadow { background: var(--amber-bg); color: var(--amber); }
|
|
249
249
|
.badge-pre { background: var(--purple-bg); color: var(--purple); }
|
|
250
250
|
.badge-trigger { background: var(--bg-tertiary); color: var(--text-secondary); font-size: 0.7rem; }
|
|
251
|
+
.badge-intent { background: var(--blue-bg); color: var(--blue); font-size: 0.7rem; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle; }
|
|
251
252
|
|
|
252
|
-
.backup-summary-cell { max-width:
|
|
253
|
+
.backup-summary-cell { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
253
254
|
.backup-summary-cell .badge-trigger { margin-right: 4px; }
|
|
255
|
+
.backup-summary-cell .badge-intent { margin-left: 4px; }
|
|
254
256
|
.backup-summary-cell .text-sm { font-size: 0.75rem; }
|
|
255
257
|
|
|
256
258
|
/* ── Stats Row ────────────────────────────────────────────── */
|
|
@@ -41,15 +41,24 @@ function entryToMs(entry) {
|
|
|
41
41
|
return d ? d.getTime() : 0;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const TRAILER_MAP = {
|
|
45
|
+
'Files-Changed': { key: 'filesChanged', parse: v => parseInt(v, 10) },
|
|
46
|
+
'Summary': { key: 'summary' },
|
|
47
|
+
'Trigger': { key: 'trigger' },
|
|
48
|
+
'Intent': { key: 'intent' },
|
|
49
|
+
'Agent': { key: 'agent' },
|
|
50
|
+
'Session': { key: 'session' },
|
|
51
|
+
};
|
|
52
|
+
|
|
44
53
|
function parseCommitTrailers(body) {
|
|
45
54
|
if (!body) return {};
|
|
46
55
|
const result = {};
|
|
56
|
+
const pattern = new RegExp(`^(${Object.keys(TRAILER_MAP).join('|')}):\\s*(.+)$`);
|
|
47
57
|
for (const line of body.split('\n')) {
|
|
48
|
-
const m = line.match(
|
|
58
|
+
const m = line.match(pattern);
|
|
49
59
|
if (m) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
result[key] = key === 'filesChanged' ? parseInt(m[2], 10) : m[2];
|
|
60
|
+
const def = TRAILER_MAP[m[1]];
|
|
61
|
+
result[def.key] = def.parse ? def.parse(m[2]) : m[2];
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
return result;
|
|
@@ -65,6 +65,9 @@ function buildCommitMessage(ts, opts) {
|
|
|
65
65
|
if (ctx.changedFileCount != null) trailers.push(`Files-Changed: ${ctx.changedFileCount}`);
|
|
66
66
|
if (ctx.summary) trailers.push(`Summary: ${ctx.summary}`);
|
|
67
67
|
if (ctx.trigger) trailers.push(`Trigger: ${ctx.trigger}`);
|
|
68
|
+
if (ctx.intent) trailers.push(`Intent: ${ctx.intent}`);
|
|
69
|
+
if (ctx.agent) trailers.push(`Agent: ${ctx.agent}`);
|
|
70
|
+
if (ctx.session) trailers.push(`Session: ${ctx.session}`);
|
|
68
71
|
|
|
69
72
|
if (trailers.length === 0) return subject;
|
|
70
73
|
return subject + '\n\n' + trailers.join('\n');
|
|
@@ -84,7 +87,10 @@ function buildCommitMessage(ts, opts) {
|
|
|
84
87
|
* @param {object} [opts.context] - Backup context metadata
|
|
85
88
|
* @param {string} [opts.context.trigger] - 'auto' | 'manual' | 'pre-restore'
|
|
86
89
|
* @param {number} [opts.context.changedFileCount] - Number of changed files
|
|
87
|
-
* @param {string} [opts.context.summary] - Short change summary (e.g. "
|
|
90
|
+
* @param {string} [opts.context.summary] - Short change summary (e.g. "Modified 3: a.js, b.js; Added 1: c.js")
|
|
91
|
+
* @param {string} [opts.context.intent] - Why this snapshot was created (e.g. "refactoring auth middleware")
|
|
92
|
+
* @param {string} [opts.context.agent] - AI model identifier (e.g. "claude-4-opus")
|
|
93
|
+
* @param {string} [opts.context.session] - Conversation/session ID
|
|
88
94
|
* @returns {{ status: 'created'|'skipped'|'error', commitHash?: string, shortHash?: string, fileCount?: number, reason?: string, error?: string, secretsExcluded?: string[] }}
|
|
89
95
|
*/
|
|
90
96
|
function createGitSnapshot(projectDir, cfg, opts = {}) {
|
package/references/mcp/server.js
CHANGED
|
@@ -92,8 +92,11 @@ server.tool(
|
|
|
92
92
|
strategy: z.enum(['git', 'shadow', 'both']).optional().describe('Backup strategy (default: from config, or "git")'),
|
|
93
93
|
message: z.string().optional().describe('Custom commit message for git snapshot'),
|
|
94
94
|
scope: z.enum(['protected', 'all']).optional().describe('Snapshot scope: "protected" = only files matching protect patterns (default when protect is configured); "all" = all files regardless of protect config'),
|
|
95
|
+
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")'),
|
|
96
|
+
agent: z.string().optional().describe('AI model identifier (e.g. "claude-4-opus")'),
|
|
97
|
+
session: z.string().optional().describe('Conversation or session ID for audit trail'),
|
|
95
98
|
},
|
|
96
|
-
async ({ path: projectPath, strategy, message, scope }) => {
|
|
99
|
+
async ({ path: projectPath, strategy, message, scope, intent, agent, session }) => {
|
|
97
100
|
const resolved = path.resolve(projectPath);
|
|
98
101
|
const { loadConfig } = require('../lib/utils');
|
|
99
102
|
const { cfg } = loadConfig(resolved);
|
|
@@ -109,10 +112,14 @@ server.tool(
|
|
|
109
112
|
const results = {};
|
|
110
113
|
|
|
111
114
|
if (effectiveStrategy === 'git' || effectiveStrategy === 'both') {
|
|
115
|
+
const context = { trigger: 'manual' };
|
|
116
|
+
if (intent) context.intent = intent;
|
|
117
|
+
if (agent) context.agent = agent;
|
|
118
|
+
if (session) context.session = session;
|
|
112
119
|
results.git = createGitSnapshot(resolved, cfg, {
|
|
113
120
|
branchRef: 'refs/guard/snapshot',
|
|
114
121
|
message: message || `guard: manual snapshot ${new Date().toISOString()}`,
|
|
115
|
-
context
|
|
122
|
+
context,
|
|
116
123
|
});
|
|
117
124
|
}
|
|
118
125
|
|