cursor-guard 4.3.2 → 4.3.4
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 +14 -0
- package/README.zh-CN.md +14 -0
- package/ROADMAP.md +40 -18
- package/SKILL.md +7 -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/auto-backup.js +23 -8
- package/references/lib/core/backups.js +13 -4
- package/references/lib/core/restore.js +17 -3
- package/references/lib/core/snapshot.js +7 -1
- package/references/lib/utils.js +21 -9
- package/references/mcp/server.js +9 -2
package/README.md
CHANGED
|
@@ -377,6 +377,20 @@ The skill activates on these signals:
|
|
|
377
377
|
|
|
378
378
|
## Changelog
|
|
379
379
|
|
|
380
|
+
### v4.3.4
|
|
381
|
+
|
|
382
|
+
- **Improve**: Log rotation — `backup.log` now rotates at 1MB, keeping up to 3 old files (`backup.log.1`, `.2`, `.3`). Rotation runs on watcher startup and every 100 writes
|
|
383
|
+
- **Improve**: Watcher single-instance protection — lock file now includes startup timestamp; locks older than 24h are auto-cleaned even if PID check is unreliable on Windows
|
|
384
|
+
- **Improve**: `previewProjectRestore` output grouped — protected paths (`.cursor/`, `.gitignore`, `.cursor-guard.json`) summarized as `protectedPaths: { count: N }` instead of listing thousands of individual files, drastically reducing token cost
|
|
385
|
+
- **Improve**: SKILL.md Hard Rule #15 — agents must commit skill files after upgrade to ensure `restore_project` HEAD protection works correctly
|
|
386
|
+
|
|
387
|
+
### v4.3.3
|
|
388
|
+
|
|
389
|
+
- **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
|
|
390
|
+
- **Feature**: Dashboard displays intent badge in backup table and full intent/agent/session fields in restore-point drawer
|
|
391
|
+
- **Improve**: `parseCommitTrailers` refactored to a data-driven map, supporting all 6 trailer fields (Files-Changed, Summary, Trigger, Intent, Agent, Session)
|
|
392
|
+
- **Improve**: SKILL.md updated to guide AI agents to pass `intent` describing the operation about to happen
|
|
393
|
+
|
|
380
394
|
### v4.3.2
|
|
381
395
|
|
|
382
396
|
- **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,20 @@ node references\dashboard\server.js --path "D:\MyProject"
|
|
|
377
377
|
|
|
378
378
|
## 更新日志
|
|
379
379
|
|
|
380
|
+
### v4.3.4
|
|
381
|
+
|
|
382
|
+
- **改进**:日志轮转——`backup.log` 超过 1MB 自动轮转,保留最近 3 个旧文件。watcher 启动时和每 100 次写入时检查
|
|
383
|
+
- **改进**:Watcher 单实例保护——锁文件新增启动时间戳;超过 24 小时的锁即使 PID 检查不可靠(Windows)也自动清理
|
|
384
|
+
- **改进**:`previewProjectRestore` 分组输出——受保护路径(`.cursor/`、`.gitignore`、`.cursor-guard.json`)汇总为 `protectedPaths: { count: N }`,不再逐一列出数千个文件,大幅降低 token 消耗
|
|
385
|
+
- **改进**:SKILL.md 硬规则 #15——升级后 Agent 必须提交 skill 文件,确保 `restore_project` 的 HEAD 保护机制生效
|
|
386
|
+
|
|
387
|
+
### v4.3.3
|
|
388
|
+
|
|
389
|
+
- **功能**:快照意图上下文——`snapshot_now` 新增 `intent`(操作意图)、`agent`(AI 模型)、`session`(会话 ID)参数,作为 Git commit trailer 存储,形成按操作事件的审计链
|
|
390
|
+
- **功能**:仪表盘备份表格显示意图徽章,恢复点抽屉完整展示 intent/agent/session 字段
|
|
391
|
+
- **改进**:`parseCommitTrailers` 重构为数据驱动映射表,支持全部 6 个 trailer 字段
|
|
392
|
+
- **改进**:SKILL.md 更新指引,要求 AI agent 在调用 `snapshot_now` 时传入 `intent` 描述即将执行的操作
|
|
393
|
+
|
|
380
394
|
### v4.3.2
|
|
381
395
|
|
|
382
396
|
- **修复**:`cursor-guard-init` 现在会将根目录 `node_modules/` 加入 `.gitignore`——防止 `npm install cursor-guard --save-dev` 后 `git add -A` 扫描数千个依赖文件导致极度缓慢
|
package/ROADMAP.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
|
|
4
4
|
> 每一代向下兼容,低版本功能永远不废弃。
|
|
5
5
|
>
|
|
6
|
-
> **当前版本**:`V4.
|
|
7
|
-
> **文档状态**:`V2` ~ `V4.
|
|
6
|
+
> **当前版本**:`V4.3.3`
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.3` 已实施(含 V5 intent 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
|---|---|---|---|
|
|
21
21
|
| `V2` | 能用 | Skill + Script | "AI 弄丢代码能恢复" |
|
|
22
22
|
| `V3` | 更稳 | + Core 抽取 + 可选 MCP | "恢复操作更标准、更省 token" |
|
|
23
|
-
| `V4` | 更聪明 | + 主动检测 + 可观测 | "cursor-guard
|
|
24
|
-
| `V5` | 成闭环 | + 变更控制层 | "AI 代码变更可预防、可追溯、可按事件恢复" |
|
|
23
|
+
| `V4` | 更聪明 | + 主动检测 + 可观测 + Web 仪表盘 + Intent 基础 | "cursor-guard 会主动提醒你,还能看到为什么备份" ✅ |
|
|
24
|
+
| `V5` | 成闭环 | + 变更控制层 | "AI 代码变更可预防、可追溯、可按事件恢复"(intent 基础已在 V4.3 落地) |
|
|
25
25
|
| `V6` | 成标准 | + 开放协议 + 团队工作流 | "把 AI 代码变更安全做成跨工具标准" |
|
|
26
26
|
| `V7` | 可证明 | + 可验证信任 + 治理层 | "能证明安全流程被执行了" |
|
|
27
27
|
|
|
@@ -64,7 +64,9 @@ V2-V3 的目标是让用户"离不开",V6-V7 的目标是让行业"绕不开"
|
|
|
64
64
|
| `Node Core` | `V3.0` ✅ 已完成 | 6 个模块:doctor / doctor-fix / snapshot / backups / restore / status | 否 |
|
|
65
65
|
| `MCP Server` | `V3.1-V4.0` ✅ 已完成 | 9 个工具,Agent 标准调用入口,结构化 JSON 返回 | 否 |
|
|
66
66
|
| `智能提醒 / 可观测` | `V4.0` ✅ 已完成 | 主动发现风险、汇总健康状态(anomaly + dashboard) | 否 |
|
|
67
|
-
|
|
|
67
|
+
| `Web 仪表盘` | `V4.2` ✅ 已完成 | 本地只读 Web UI,备份/恢复点/诊断/保护范围,中英双语 | 否 |
|
|
68
|
+
| `备份上下文 + Intent` | `V4.3` ✅ 已完成 | 结构化 commit trailer(Files-Changed/Summary/Trigger/Intent/Agent/Session),仪表盘可追溯 | 否 |
|
|
69
|
+
| `变更控制层` | `V5` 规划(intent 基础已在 V4.3 落地) | 覆盖编辑意图、冲突告警、影响半径、审计事件、按事件恢复 | 否 |
|
|
68
70
|
| `开放协议 / 团队工作流` | `V6` 规划 | 把变更控制能力提炼成跨工具协议、适配器和 CI 查询入口 | 否 |
|
|
69
71
|
| `治理 / 可验证层` | `V7` 规划 | 让“是否走过安全流程”变成可以证明、可以审计、可以验证的事实 | 否 |
|
|
70
72
|
|
|
@@ -440,10 +442,25 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
440
442
|
| 三审 | 4 项 | `doctor-fix.js` stale-lock PID 正则修正(`pid=` 格式);Git pre-restore ref 加毫秒防碰撞;非 Git pre-restore shadow 同步加碰撞检测;`doctor.js` shadow 目录正则支持毫秒后缀 |
|
|
441
443
|
| 四审 | 3+2 项 | `createGitSnapshot` protect basename 语义对齐(`git add -A` + `matchesAny` 裁剪替代 `git add -- <pattern>`);`unquoteGitPath()` 反解 C-quoted 路径(支持 UTF-8 octal);pre-restore ref / shadow snapshot 碰撞重试循环(最多 1000 次);`previewProjectRestore` 正确解析 rename/copy 条目 |
|
|
442
444
|
|
|
445
|
+
### V4.x 增量(全部已完成 ✅)
|
|
446
|
+
|
|
447
|
+
| 版本 | 新增 | 状态 |
|
|
448
|
+
|---|---|---|
|
|
449
|
+
| V4.1 | 用户反馈修复:fileCount 精度、安装流程、doctor MCP 检测、PowerShell 兼容 | ✅ |
|
|
450
|
+
| V4.2.0 | **Web 仪表盘**:本地只读 Web UI,健康总览、备份表格、恢复点抽屉、诊断、保护范围。中英双语、自动刷新、多项目支持 | ✅ |
|
|
451
|
+
| V4.2.1 | 代码审查修复:`t()` replaceAll、未用导入清理、过滤栏补全、跨平台 shell 兼容、去重 | ✅ |
|
|
452
|
+
| V4.2.2 | `restore_project` 保护 `.cursor-guard.json`;init 提示 `git commit` | ✅ |
|
|
453
|
+
| V4.3.0 | **备份上下文元数据**:Git commit 结构化 trailer(Files-Changed / Summary / Trigger),仪表盘"变更"列 | ✅ |
|
|
454
|
+
| V4.3.1 | `restore_project` 保护 `.gitignore`;`cursor-guard-index.lock` 清理;summary 按 protect/ignore 过滤 + 分类格式 | ✅ |
|
|
455
|
+
| V4.3.2 | `cursor-guard-init` 自动添加根目录 `node_modules/` 到 `.gitignore`;doctor MCP 版本提示含重载快捷键 | ✅ |
|
|
456
|
+
| V4.3.3 | **Intent 上下文**(V5 基础前置):`snapshot_now` 支持 `intent` / `agent` / `session` 参数,Git trailer 存储,仪表盘展示意图徽章和完整审计字段 | ✅ |
|
|
457
|
+
|
|
458
|
+
> **注**:V4.2 的 Web 仪表盘最初在 V4.0 规划中标记为"不做",但用户需求明确后实施。事实证明只读仪表盘投入产出比合理,且不违反安全原则。
|
|
459
|
+
|
|
443
460
|
### V4 不做的事
|
|
444
461
|
|
|
445
462
|
- 不做自动恢复(恢复永远需要人确认,这是产品底线)
|
|
446
|
-
-
|
|
463
|
+
- ~~不做 Web 仪表盘~~ → V4.2 已实施(只读、本地、零依赖)
|
|
447
464
|
- 不做云端同步
|
|
448
465
|
|
|
449
466
|
### 进入 V5 的衡量标准
|
|
@@ -494,10 +511,10 @@ V5 不是"三个方向选一个",而是把下面这条链路做完整:
|
|
|
494
511
|
|
|
495
512
|
| 模块 / 能力 | AI 要做什么 | 建议产物 | 完成标准 |
|
|
496
513
|
|---|---|---|---|
|
|
497
|
-
| `intent registry` | 在高风险写入前注册编辑意图,记录 agent、会话、工作区、分支、目标文件、风险级别 | `core/intent.*` 或同等模块 |
|
|
514
|
+
| `intent registry` | 在高风险写入前注册编辑意图,记录 agent、会话、工作区、分支、目标文件、风险级别 | `core/intent.*` 或同等模块 | 能列出活跃会话,能释放会话,能查到谁准备改哪个文件。**基础版已在 V4.3.3 落地**:`snapshot_now` 支持 `intent` / `agent` / `session` 参数,存储为 Git commit trailer,仪表盘可展示 |
|
|
498
515
|
| `pre-edit snapshot` | 在每次高风险 AI 写入前创建 `refs/guard/pre-edit/*` 恢复点 | `refs/guard/pre-edit/<session>/<seq>` | 任意一条 AI 编辑事件都能关联到写前快照 |
|
|
499
516
|
| `conflict detection` | 先做文件路径级冲突检测,再预留符号级增强位 | `detectConflicts()` / `listConflicts()` | 两个会话同时改重叠文件时能给出 advisory warning |
|
|
500
|
-
| `audit store` | 以 append-only 方式保存 AI 编辑事件 | 默认本地 `JSONL`;后续可升级 `SQLite` | 能按文件 / 会话 / agent / 时间 /
|
|
517
|
+
| `audit store` | 以 append-only 方式保存 AI 编辑事件 | 默认本地 `JSONL`;后续可升级 `SQLite` | 能按文件 / 会话 / agent / 时间 / 风险级别查询。**雏形已在 V4.3.0-V4.3.3 落地**:审计元数据通过 Git commit trailer 持久化,`listBackups` 可按 trigger/intent/agent/session 解析 |
|
|
501
518
|
| `restore by event` | 允许从一条审计事件直接跳转并执行恢复 | `restore_from_event` | 给定 `event_id` 可以定位 `before_ref` 或 `restore_ref` |
|
|
502
519
|
| `impact set` | 为高风险编辑记录受影响文件 / 符号 / 测试集合 | `impact_set` 字段 | 查询事件时能看到"这次改动可能波及哪里" |
|
|
503
520
|
| `MCP / CLI surface` | 暴露最小可用接口给 Agent 和终端 | `register_intent` / `list_active_intents` / `audit_query` / `get_event` / `restore_from_event` | AI 不需要拼复杂 shell,就能完成查询与恢复 |
|
|
@@ -935,8 +952,8 @@ V7.2 完整 attestation(安全操作证明链)
|
|
|
935
952
|
|
|
936
953
|
| | V2 | V3 | V4 | V5 | V6 | V7 |
|
|
937
954
|
|---|---|---|---|---|---|---|
|
|
938
|
-
| **一句话** | 能恢复 | 更稳更省 | 主动提醒 | 变更闭环 | 跨工具标准 | 可证明 |
|
|
939
|
-
| **核心架构** | Skill + Script | + Core + MCP | + 智能检测 | + 变更控制层 | + 开放协议 + 适配器 | + 治理层 |
|
|
955
|
+
| **一句话** | 能恢复 | 更稳更省 | 主动提醒 + 可观测 + Intent | 变更闭环 | 跨工具标准 | 可证明 |
|
|
956
|
+
| **核心架构** | Skill + Script | + Core + MCP | + 智能检测 + Web 仪表盘 + Intent 基础 | + 变更控制层 | + 开放协议 + 适配器 | + 治理层 |
|
|
940
957
|
| **Agent 调用** | 拼 shell | 优先 MCP | MCP + 主动建议 | MCP + 意图 / 审计 / 恢复 | 标准接口 + 适配器 | 标准接口 + 审计 |
|
|
941
958
|
| **安装门槛** | 最低 | 不变 | 不变 | 略增 | 看具体实现 | 看具体实现 |
|
|
942
959
|
| **适合谁** | 所有人 | 所有人 | 所有人 | 重度 AI 用户 + 团队试点 | 工具开发者 + 团队 | 企业 + 合规场景 |
|
|
@@ -957,8 +974,11 @@ V3.4 ────── ✅ MCP 自检
|
|
|
957
974
|
│
|
|
958
975
|
│ 前提:MCP 调用成功率 > 95%,token 消耗可观测下降
|
|
959
976
|
▼
|
|
960
|
-
V4.0 ────── ✅ 智能恢复建议 + 备份健康看板 + 4 轮代码审查加固(138 测试)
|
|
961
|
-
V4.
|
|
977
|
+
V4.0 ────── ✅ 智能恢复建议 + 备份健康看板 + 4 轮代码审查加固(138 测试)
|
|
978
|
+
V4.1 ────── ✅ 用户反馈修复
|
|
979
|
+
V4.2 ────── ✅ Web 仪表盘(只读、双语、多项目)+ 代码审查修复
|
|
980
|
+
V4.3 ────── ✅ 备份上下文元数据 + Intent 上下文 + restore 安全加固 ← 当前版本
|
|
981
|
+
V4.x ────── 候选支线(完整性校验 / 更丰富的审计查询)
|
|
962
982
|
│
|
|
963
983
|
│ 前提:AI 编辑需要更强的追溯 / 恢复 / 查询闭环
|
|
964
984
|
│ 前提:多 Agent / 多工具协作成为真实场景
|
|
@@ -1002,13 +1022,15 @@ V7 的"可验证治理"是这条产品线的逻辑终点——该保护的都保
|
|
|
1002
1022
|
|
|
1003
1023
|
## 给用户说的话
|
|
1004
1024
|
|
|
1005
|
-
### 现在(V4)
|
|
1025
|
+
### 现在(V4.3)
|
|
1006
1026
|
|
|
1007
1027
|
> cursor-guard 已经能保护你的代码,而且越来越聪明。
|
|
1008
1028
|
> 自动备份、写前快照、确定性恢复——开箱即用。
|
|
1009
1029
|
> V3 新增:MCP 工具调用(可选)让 AI 操作更稳、更快、更省 token。
|
|
1010
|
-
> V4 新增:系统会主动监测异常变更并提醒你,一个 `dashboard` 就能看全局健康状态。
|
|
1011
|
-
>
|
|
1030
|
+
> V4.0 新增:系统会主动监测异常变更并提醒你,一个 `dashboard` 就能看全局健康状态。
|
|
1031
|
+
> V4.2 新增:本地 Web 仪表盘——健康、备份、恢复点、诊断一页可见,中英双语自动刷新。
|
|
1032
|
+
> V4.3 新增:每次备份带上下文(改了什么、为什么备份、哪个 AI 在操作),在仪表盘可追溯。
|
|
1033
|
+
> 经过 4 轮代码审查,138+ 个测试覆盖所有核心路径。
|
|
1012
1034
|
|
|
1013
1035
|
### 未来
|
|
1014
1036
|
|
|
@@ -1027,7 +1049,7 @@ V7 的"可验证治理"是这条产品线的逻辑终点——该保护的都保
|
|
|
1027
1049
|
| 自动恢复(无人确认) | 恢复操作必须有人确认,这是产品底线 |
|
|
1028
1050
|
| 自动 push 到远程 | 本地优先,push 需用户明确指令 |
|
|
1029
1051
|
| 云端备份服务 | cursor-guard 的定位是本地安全网 |
|
|
1030
|
-
| Web
|
|
1052
|
+
| ~~Web 仪表盘~~ | ~~投入产出比不合理~~ → V4.2 已实施只读仪表盘(本地、零依赖、不执行写操作) |
|
|
1031
1053
|
| 取代 Git | cursor-guard 是 Git 的增强,不是替代 |
|
|
1032
1054
|
| AI 行为限制 | cursor-guard 是安全网,不是笼子 |
|
|
1033
1055
|
| 多 IDE 全面适配 | 聚焦 Cursor,其他 IDE 由社区或 V6 协议解决 |
|
|
@@ -1036,5 +1058,5 @@ V7 的"可验证治理"是这条产品线的逻辑终点——该保护的都保
|
|
|
1036
1058
|
|
|
1037
1059
|
---
|
|
1038
1060
|
|
|
1039
|
-
*最后更新:2026-03-
|
|
1040
|
-
*版本:v1.
|
|
1061
|
+
*最后更新:2026-03-22*
|
|
1062
|
+
*版本:v1.3(V4.3.3 交付,含 Web 仪表盘、备份上下文元数据、Intent 基础)*
|
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
|
|
|
@@ -579,6 +583,7 @@ Skip the block for unrelated turns.
|
|
|
579
583
|
12. **Concurrent agents**: if multiple Agent threads are active, warn the user to avoid simultaneous writes to the same file. Snapshots cannot prevent race conditions between parallel agents.
|
|
580
584
|
13. **Preservation must not pollute** — all pre-restore backups use temp index + dedicated ref (`refs/guard/pre-restore`). The user's staging area, working tree, and commit history on their branch are never modified by the preservation process.
|
|
581
585
|
14. **Do not restore `.cursor/` directory** — `.cursor/skills/`, `.cursor/rules/`, and `.cursor/mcp.json` are tool infrastructure, not project code. `restore_project` automatically protects this directory. Never include `.cursor/` in manual restore commands unless the user explicitly requests it. Restoring `.cursor/skills/cursor-guard/` would downgrade the protection tool itself.
|
|
586
|
+
15. **Commit skill files after upgrade** — after running `cursor-guard-init` or upgrading the skill, immediately commit `.cursor/skills/cursor-guard/` and `.cursor-guard.json` to HEAD (`git add .cursor/ .cursor-guard.json && git commit -m "chore: upgrade cursor-guard"`). This ensures `restore_project`'s HEAD protection mechanism can preserve the current skill version. Without this commit, a restore could revert the skill to an older version.
|
|
582
587
|
|
|
583
588
|
---
|
|
584
589
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.4",
|
|
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 ────────────────────────────────────────────── */
|
|
@@ -72,24 +72,39 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
72
72
|
// Ensure backup dir
|
|
73
73
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
74
74
|
|
|
75
|
-
// Lock file with stale detection
|
|
75
|
+
// Lock file with stale detection (PID + age)
|
|
76
|
+
const LOCK_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
76
77
|
if (fs.existsSync(lockFile)) {
|
|
77
78
|
let stale = false;
|
|
79
|
+
let reason = '';
|
|
78
80
|
try {
|
|
79
81
|
const content = fs.readFileSync(lockFile, 'utf-8');
|
|
80
82
|
const pidMatch = content.match(/pid=(\d+)/);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const startedMatch = content.match(/started=(.+)/);
|
|
84
|
+
const oldPid = pidMatch ? parseInt(pidMatch[1], 10) : null;
|
|
85
|
+
const startedAt = startedMatch ? Date.parse(startedMatch[1]) : NaN;
|
|
86
|
+
|
|
87
|
+
if (oldPid && !isProcessAlive(oldPid)) {
|
|
88
|
+
stale = true;
|
|
89
|
+
reason = `pid ${oldPid} not running`;
|
|
90
|
+
} else if (!isNaN(startedAt) && Date.now() - startedAt > LOCK_MAX_AGE_MS) {
|
|
91
|
+
stale = true;
|
|
92
|
+
reason = `lock age > 24h (started ${startedMatch[1]})`;
|
|
93
|
+
} else if (!oldPid) {
|
|
94
|
+
const stat = fs.statSync(lockFile);
|
|
95
|
+
if (Date.now() - stat.mtimeMs > LOCK_MAX_AGE_MS) {
|
|
84
96
|
stale = true;
|
|
85
|
-
|
|
86
|
-
fs.unlinkSync(lockFile);
|
|
97
|
+
reason = 'lock file too old (no PID, mtime > 24h)';
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
100
|
} catch { /* ignore */ }
|
|
90
|
-
if (
|
|
101
|
+
if (stale) {
|
|
102
|
+
console.log(color.yellow(`[guard] Stale lock detected (${reason}). Cleaning up.`));
|
|
103
|
+
try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
|
|
104
|
+
} else {
|
|
91
105
|
console.log(color.red(`[guard] ERROR: Lock file exists (${lockFile}).`));
|
|
92
|
-
console.log(color.red('
|
|
106
|
+
console.log(color.red(' Another watcher instance may be running.'));
|
|
107
|
+
console.log(color.red(' If no other instance is running, delete the lock file and retry.'));
|
|
93
108
|
process.exit(1);
|
|
94
109
|
}
|
|
95
110
|
}
|
|
@@ -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;
|
|
@@ -170,7 +170,7 @@ function restoreFile(projectDir, file, source, opts = {}) {
|
|
|
170
170
|
*
|
|
171
171
|
* @param {string} projectDir
|
|
172
172
|
* @param {string} source - Commit hash or ref
|
|
173
|
-
* @returns {{ status: 'ok'|'error', files?: Array<{path: string, change:
|
|
173
|
+
* @returns {{ status: 'ok'|'error', files?: Array<{path: string, change: string}>, protectedPaths?: {count: number, note?: string}, totalChanged?: number, error?: string }}
|
|
174
174
|
*/
|
|
175
175
|
function previewProjectRestore(projectDir, source) {
|
|
176
176
|
if (!isGitRepo(projectDir)) {
|
|
@@ -218,13 +218,27 @@ function previewProjectRestore(projectDir, source) {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
const protectedFiles = [];
|
|
222
|
+
const projectFiles = [];
|
|
221
223
|
for (const f of files) {
|
|
222
224
|
if (isToolPath(f.path)) {
|
|
223
|
-
f
|
|
225
|
+
protectedFiles.push(f);
|
|
226
|
+
} else {
|
|
227
|
+
projectFiles.push(f);
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
status: 'ok',
|
|
233
|
+
files: projectFiles,
|
|
234
|
+
protectedPaths: {
|
|
235
|
+
count: protectedFiles.length,
|
|
236
|
+
note: protectedFiles.length > 0
|
|
237
|
+
? 'these paths (.cursor/, .cursor-guard.json, .gitignore) will be preserved from HEAD'
|
|
238
|
+
: undefined,
|
|
239
|
+
},
|
|
240
|
+
totalChanged: files.length,
|
|
241
|
+
};
|
|
228
242
|
} catch (e) {
|
|
229
243
|
return { status: 'error', error: e.message };
|
|
230
244
|
}
|
|
@@ -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/lib/utils.js
CHANGED
|
@@ -354,19 +354,31 @@ function timestamp() {
|
|
|
354
354
|
return new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
function createLogger(logFilePath, maxSizeMB =
|
|
357
|
+
function createLogger(logFilePath, { maxSizeMB = 1, maxFiles = 3 } = {}) {
|
|
358
358
|
let writeCount = 0;
|
|
359
|
-
|
|
360
|
-
|
|
359
|
+
const maxBytes = maxSizeMB * 1024 * 1024;
|
|
360
|
+
|
|
361
|
+
function rotate() {
|
|
361
362
|
try {
|
|
362
363
|
const stat = fs.statSync(logFilePath);
|
|
363
|
-
if (stat.size
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
364
|
+
if (stat.size <= maxBytes) return;
|
|
365
|
+
} catch { return; }
|
|
366
|
+
for (let i = maxFiles - 1; i >= 1; i--) {
|
|
367
|
+
const from = i === 1 ? logFilePath + '.1' : logFilePath + '.' + i;
|
|
368
|
+
const to = logFilePath + '.' + (i + 1);
|
|
369
|
+
try { fs.renameSync(from, to); } catch { /* ignore */ }
|
|
370
|
+
}
|
|
371
|
+
try { fs.renameSync(logFilePath, logFilePath + '.1'); } catch { /* ignore */ }
|
|
372
|
+
try { fs.unlinkSync(logFilePath + '.' + (maxFiles + 1)); } catch { /* ignore */ }
|
|
369
373
|
}
|
|
374
|
+
|
|
375
|
+
rotate();
|
|
376
|
+
|
|
377
|
+
function rotateIfNeeded() {
|
|
378
|
+
if (++writeCount % 100 !== 0) return;
|
|
379
|
+
rotate();
|
|
380
|
+
}
|
|
381
|
+
|
|
370
382
|
return {
|
|
371
383
|
log(msg, c = 'green') {
|
|
372
384
|
const line = `${timestamp()} ${msg}`;
|
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
|
|