cursor-guard 4.6.1 → 4.7.0

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/LICENSE CHANGED
@@ -1,21 +1,65 @@
1
- MIT License
2
-
3
- Copyright (c) 2026
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: zhangqiang8vipp
6
+ Licensed Work: cursor-guard
7
+ The Licensed Work is (c) 2026 zhangqiang8vipp
8
+ Additional Use Grant: You may make use of the Licensed Work, provided that
9
+ you may not use the Licensed Work for a Commercial Use.
10
+ "Commercial Use" means distribution, sale, licensing,
11
+ sublicensing, or providing the Licensed Work (or any
12
+ derivative work) to third parties as a paid product or
13
+ service, or incorporating it into a paid product or
14
+ service offered to third parties.
15
+ Change Date: 2056-03-22
16
+ Change License: Apache License, Version 2.0
17
+
18
+ For information about alternative licensing arrangements for the Licensed Work,
19
+ please contact: zhangqiang8vipp
20
+
21
+ Notice
22
+
23
+ Business Source License 1.1
24
+
25
+ Terms
26
+
27
+ The Licensor hereby grants you the right to copy, modify, create derivative
28
+ works, redistribute, and make non-commercial use of the Licensed Work. The
29
+ Licensor may make an Additional Use Grant, above, permitting limited commercial
30
+ use.
31
+
32
+ Effective on the Change Date, or the fourth anniversary of the first publicly
33
+ available distribution of a specific version of the Licensed Work under this
34
+ License, whichever comes first, the Licensor hereby grants you rights under
35
+ the terms of the Change License, and the rights granted in the paragraph
36
+ above terminate.
37
+
38
+ If your use of the Licensed Work does not comply with the requirements
39
+ currently in effect as described in this License, you must purchase a
40
+ commercial license from the Licensor, its affiliated entities, or authorized
41
+ resellers, or you must refrain from using the Licensed Work.
42
+
43
+ All copies of the original and modified Licensed Work, and derivative works
44
+ of the Licensed Work, are subject to this License. This License applies
45
+ separately for each version of the Licensed Work and the Change Date may vary
46
+ for each version of the Licensed Work released by Licensor.
47
+
48
+ You must conspicuously display this License on each original or modified copy
49
+ of the Licensed Work. If you receive the Licensed Work in original or
50
+ modified form from a third party, the terms and conditions set forth in this
51
+ License apply to your use of that work.
52
+
53
+ Any use of the Licensed Work in violation of this License will automatically
54
+ terminate your rights under this License for the current and all other
55
+ versions of the Licensed Work.
56
+
57
+ This License does not grant you any right in any trademark or logo of
58
+ Licensor or its affiliates (provided that you may use a trademark or logo of
59
+ Licensor as expressly required by this License).
60
+
61
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
62
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
63
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
64
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
65
+ TITLE.
package/README.md CHANGED
@@ -274,7 +274,7 @@ npx cursor-guard-dashboard --path /my/project --port 8080
274
274
  node references\dashboard\server.js --path "D:\MyProject"
275
275
  ```
276
276
 
277
- Then open `http://127.0.0.1:3120` in your browser.
277
+ Then open `http://127.0.0.1:3120` in your browser. Or use the **IDE Extension** (see below) to embed the dashboard directly in your editor.
278
278
 
279
279
  Features:
280
280
 
@@ -287,6 +287,29 @@ Features:
287
287
  - **Security** — binds to `127.0.0.1` only (not exposed to LAN), API uses project IDs instead of raw file paths, static file serving restricted to `public/` directory
288
288
  - **Zero extra dependencies** — uses Node.js built-in `http` module + existing cursor-guard core modules
289
289
 
290
+ ### IDE Extension (VSCode / Cursor)
291
+
292
+ Embed the full dashboard directly inside your IDE — no browser needed.
293
+
294
+ The extension is located at `references/vscode-extension/`. To install:
295
+
296
+ ```bash
297
+ # From the cursor-guard skill directory
298
+ cd references/vscode-extension
299
+ # Install as a development extension in your IDE
300
+ code --install-extension .
301
+ ```
302
+
303
+ Features:
304
+
305
+ - **WebView Dashboard** — full dashboard embedded as an editor tab, identical to the browser version
306
+ - **Status Bar Indicator** — shows `Guard: OK` (green) or `Guard: 22 files!` (yellow) in real-time
307
+ - **Sidebar TreeView** — activity bar icon with project list, watcher status, backup stats, alerts, health
308
+ - **Command Palette** — `Cursor Guard: Open Dashboard`, `Snapshot Now`, `Start Watcher`, `Refresh`
309
+ - **Auto-activation** — detects `.cursor-guard.json` in workspace, starts dashboard server automatically
310
+ - **Multi-project** — hot-loads all workspace folders with `.cursor-guard.json`
311
+ - **Compatible** — works with VSCode ^1.74.0, Cursor, Windsurf, and all VSCode-based IDEs
312
+
290
313
  ---
291
314
 
292
315
  ## Recovery
package/README.zh-CN.md CHANGED
@@ -274,7 +274,7 @@ npx cursor-guard-dashboard --path /my/project --port 8080
274
274
  node references\dashboard\server.js --path "D:\MyProject"
275
275
  ```
276
276
 
277
- 然后在浏览器打开 `http://127.0.0.1:3120`。
277
+ 然后在浏览器打开 `http://127.0.0.1:3120`。也可以使用 **IDE 扩展**(见下方)将仪表盘直接嵌入编辑器。
278
278
 
279
279
  特性:
280
280
 
@@ -287,6 +287,29 @@ node references\dashboard\server.js --path "D:\MyProject"
287
287
  - **安全性** — 仅绑定 `127.0.0.1`(不暴露到局域网)、API 使用项目 ID 而非原始路径、静态文件服务严格限制在 `public/` 目录
288
288
  - **零额外依赖** — 使用 Node.js 内置 `http` 模块 + cursor-guard 已有核心模块
289
289
 
290
+ ### IDE 扩展(VSCode / Cursor)
291
+
292
+ 将完整仪表盘直接嵌入 IDE 内部,无需打开浏览器。
293
+
294
+ 扩展位于 `references/vscode-extension/`。安装方式:
295
+
296
+ ```bash
297
+ # 从 cursor-guard skill 目录
298
+ cd references/vscode-extension
299
+ # 作为开发扩展安装到 IDE
300
+ code --install-extension .
301
+ ```
302
+
303
+ 功能:
304
+
305
+ - **WebView 仪表盘** — 完整仪表盘作为编辑器标签页嵌入,与浏览器版本完全一致
306
+ - **状态栏指示器** — 实时显示 `Guard: OK`(绿色)或 `Guard: 22 files!`(黄色告警)
307
+ - **侧边栏 TreeView** — Activity Bar 图标,树形展示项目列表、Watcher 状态、备份统计、告警、健康评估
308
+ - **命令面板** — `Cursor Guard: Open Dashboard`、`Snapshot Now`、`Start Watcher`、`Refresh`
309
+ - **自动激活** — 检测到工作区有 `.cursor-guard.json` 时自动启动 Dashboard 服务
310
+ - **多项目** — 热加载所有包含 `.cursor-guard.json` 的工作区文件夹
311
+ - **兼容性** — 支持 VSCode ^1.74.0、Cursor、Windsurf 及所有基于 VSCode 的 IDE
312
+
290
313
  ---
291
314
 
292
315
  ## 恢复
package/ROADMAP.md CHANGED
@@ -3,8 +3,8 @@
3
3
  > 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
4
4
  > 每一代向下兼容,低版本功能永远不废弃。
5
5
  >
6
- > **当前版本**:`V4.6.1`
7
- > **文档状态**:`V2` ~ `V4.6.1` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
6
+ > **当前版本**:`V4.7.0`
7
+ > **文档状态**:`V2` ~ `V4.7.0` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
8
8
 
9
9
  ## 阅读导航
10
10
 
@@ -734,14 +734,19 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
734
734
  }
735
735
  ```
736
736
 
737
- ### V4.7 规划:IDE 集成(VSCode/Cursor Extension)
737
+ ### V4.7.0:IDE 集成(VSCode/Cursor Extension)
738
738
 
739
- | 项目 | 说明 |
739
+ | 组件 | 说明 |
740
740
  |------|------|
741
- | 定位 | Dashboard 从浏览器迁移到 IDE 内置 WebView,减少上下文切换 |
742
- | 实现方式 | VSCode Extension + WebView Panel,复用现有 `dashboard/public/` 前端代码 |
743
- | 核心能力 | IDE 侧边栏入口、状态栏告警指示器、WebView 内嵌 Dashboard、一键快照按钮 |
744
- | 状态 | 🔮 规划中 |
741
+ | `extension.js` | 扩展入口。自动检测 `.cursor-guard.json` 激活,启动内嵌 Dashboard Server,注册所有命令和视图 |
742
+ | `dashboard-manager.js` | 复用现有 `dashboard/server.js` 单例模式,在扩展宿主进程内直接 require,零额外开销。支持多 workspace folder 热加载 |
743
+ | `webview-provider.js` | WebView Panel 管理。加载 `dashboard/public/` 前端,通过 `asWebviewUri()` 转换资源路径,注入 `__GUARD_TOKEN__` + `__GUARD_BASE_URL__` + `__IN_VSCODE__` |
744
+ | `status-bar.js` | 状态栏告警指示器。正常时显示 `$(shield) Guard: OK`,告警时黄色背景 `$(warning) Guard: 22 files!`,点击打开 Dashboard |
745
+ | `tree-view.js` | Activity Bar 侧边栏。树形展示项目列表,每个项目下显示 Watcher 状态、最近备份时间、备份统计、活跃告警、健康评估 |
746
+ | `poller.js` | 后台每 5 秒轮询 `/api/page-data`,驱动状态栏和 TreeView 实时更新 |
747
+ | `app.js` 适配 | `fetchJson` 支持 `window.__GUARD_BASE_URL__` 前缀(兼容浏览器和 WebView);`copyText` 在 VSCode 中通过 `postMessage` 桥接到 `vscode.env.clipboard` |
748
+ | 命令面板 | `Open Dashboard` / `Snapshot Now` / `Start/Stop Watcher` / `Refresh` |
749
+ | 兼容性 | VSCode ^1.74.0,覆盖 Cursor、Windsurf 等所有 VSCode 衍生 IDE |
745
750
 
746
751
  ### V4 不做的事
747
752
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-guard",
3
- "version": "4.6.1",
3
+ "version": "4.7.0",
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",
@@ -14,7 +14,7 @@
14
14
  "cross-platform"
15
15
  ],
16
16
  "author": "zhangqiang8vipp",
17
- "license": "MIT",
17
+ "license": "BUSL-1.1",
18
18
  "repository": {
19
19
  "type": "git",
20
20
  "url": "https://github.com/zhangqiang8vipp/cursor-guard.git"
@@ -50,6 +50,7 @@
50
50
  "references/lib/core/",
51
51
  "references/mcp/",
52
52
  "references/dashboard/",
53
+ "references/vscode-extension/",
53
54
  "references/config-reference.md",
54
55
  "references/config-reference.zh-CN.md",
55
56
  "references/cursor-guard.example.json",
@@ -674,9 +674,10 @@ function relativeTime(ts) {
674
674
  /* ── Data fetching ────────────────────────────────────────── */
675
675
 
676
676
  async function fetchJson(url) {
677
+ const base = window.__GUARD_BASE_URL__ || '';
677
678
  const sep = url.includes('?') ? '&' : '?';
678
679
  const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
679
- const r = await fetch(url + tokenParam);
680
+ const r = await fetch(base + url + tokenParam);
680
681
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
681
682
  return r.json();
682
683
  }
@@ -1526,16 +1527,21 @@ function openDoctorDrawer() {
1526
1527
  /* ── Copy to clipboard ────────────────────────────────────── */
1527
1528
 
1528
1529
  async function copyText(text) {
1529
- try {
1530
- await navigator.clipboard.writeText(text);
1531
- } catch {
1532
- const ta = document.createElement('textarea');
1533
- ta.value = text;
1534
- ta.style.cssText = 'position:fixed;left:-9999px';
1535
- document.body.appendChild(ta);
1536
- ta.select();
1537
- document.execCommand('copy');
1538
- document.body.removeChild(ta);
1530
+ if (window.__IN_VSCODE__ && window.acquireVsCodeApi) {
1531
+ try { window.__vscodeApi = window.__vscodeApi || acquireVsCodeApi(); } catch { /* already acquired */ }
1532
+ window.__vscodeApi?.postMessage({ type: 'copy', text });
1533
+ } else {
1534
+ try {
1535
+ await navigator.clipboard.writeText(text);
1536
+ } catch {
1537
+ const ta = document.createElement('textarea');
1538
+ ta.value = text;
1539
+ ta.style.cssText = 'position:fixed;left:-9999px';
1540
+ document.body.appendChild(ta);
1541
+ ta.select();
1542
+ document.execCommand('copy');
1543
+ document.body.removeChild(ta);
1544
+ }
1539
1545
  }
1540
1546
  showToast(t('drawer.copied'));
1541
1547
  }
@@ -1704,9 +1710,10 @@ async function restartServer(banner) {
1704
1710
  banner.querySelector('.upgrade-banner-close').style.display = 'none';
1705
1711
 
1706
1712
  try {
1713
+ const base = window.__GUARD_BASE_URL__ || '';
1707
1714
  const sep = '/api/restart'.includes('?') ? '&' : '?';
1708
1715
  const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
1709
- await fetch('/api/restart' + tokenParam, { method: 'POST' });
1716
+ await fetch(base + '/api/restart' + tokenParam, { method: 'POST' });
1710
1717
  } catch { /* server may close connection */ }
1711
1718
 
1712
1719
  btn.textContent = t('upgrade.waiting');
@@ -1714,7 +1721,7 @@ async function restartServer(banner) {
1714
1721
  for (let i = 0; i < 20; i++) {
1715
1722
  await new Promise(r => setTimeout(r, 500));
1716
1723
  try {
1717
- const r = await fetch('/api/version' + (window.__GUARD_TOKEN__ ? '?token=' + window.__GUARD_TOKEN__ : ''));
1724
+ const r = await fetch((window.__GUARD_BASE_URL__ || '') + '/api/version' + (window.__GUARD_TOKEN__ ? '?token=' + window.__GUARD_TOKEN__ : ''));
1718
1725
  if (r.ok) { ready = true; break; }
1719
1726
  } catch { /* still restarting */ }
1720
1727
  }
@@ -0,0 +1,4 @@
1
+ .vscode/**
2
+ node_modules/**
3
+ *.md
4
+ .gitignore
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+ const { DashboardManager } = require('./lib/dashboard-manager');
5
+ const { WebViewProvider } = require('./lib/webview-provider');
6
+ const { StatusBarController } = require('./lib/status-bar');
7
+ const { GuardTreeView } = require('./lib/tree-view');
8
+ const { Poller } = require('./lib/poller');
9
+
10
+ let dashMgr, poller, statusBar, treeView, webviewProvider;
11
+
12
+ async function activate(context) {
13
+ dashMgr = new DashboardManager();
14
+ poller = new Poller(dashMgr);
15
+ statusBar = new StatusBarController(poller);
16
+ treeView = new GuardTreeView(poller, dashMgr);
17
+ webviewProvider = new WebViewProvider(context, dashMgr);
18
+
19
+ context.subscriptions.push(
20
+ vscode.commands.registerCommand('cursorGuard.openDashboard', () => {
21
+ if (!dashMgr.running) {
22
+ vscode.window.showWarningMessage('Cursor Guard: no projects detected. Add .cursor-guard.json to your workspace.');
23
+ return;
24
+ }
25
+ webviewProvider.show();
26
+ }),
27
+
28
+ vscode.commands.registerCommand('cursorGuard.snapshotNow', async () => {
29
+ const folders = vscode.workspace.workspaceFolders;
30
+ if (!folders || folders.length === 0) return;
31
+ const projectPath = folders[0].uri.fsPath;
32
+ const result = await dashMgr.snapshotNow(projectPath);
33
+ if (result?.status === 'created') {
34
+ vscode.window.showInformationMessage(`Cursor Guard: snapshot created (${result.changedCount || 0} changes)`);
35
+ } else if (result?.status === 'unchanged') {
36
+ vscode.window.showInformationMessage('Cursor Guard: no changes to snapshot');
37
+ } else {
38
+ vscode.window.showWarningMessage(`Cursor Guard: ${result?.error || 'snapshot failed'}`);
39
+ }
40
+ poller.forceRefresh();
41
+ }),
42
+
43
+ vscode.commands.registerCommand('cursorGuard.startWatcher', () => {
44
+ vscode.window.showInformationMessage(
45
+ 'Cursor Guard: run `cursor-guard-backup --path <dir> --dashboard` in terminal to start the watcher.'
46
+ );
47
+ }),
48
+
49
+ vscode.commands.registerCommand('cursorGuard.stopWatcher', () => {
50
+ vscode.window.showInformationMessage(
51
+ 'Cursor Guard: stop the watcher by terminating its terminal process (Ctrl+C).'
52
+ );
53
+ }),
54
+
55
+ vscode.commands.registerCommand('cursorGuard.refreshTree', () => {
56
+ poller.forceRefresh();
57
+ treeView.refresh();
58
+ }),
59
+
60
+ statusBar,
61
+ poller,
62
+ treeView,
63
+ webviewProvider,
64
+ );
65
+
66
+ const started = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
67
+ if (started) {
68
+ poller.start();
69
+ vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
70
+ }
71
+
72
+ context.subscriptions.push(
73
+ vscode.workspace.onDidChangeWorkspaceFolders(async () => {
74
+ const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
75
+ if (restarted && !poller._timer) poller.start();
76
+ poller.forceRefresh();
77
+ })
78
+ );
79
+ }
80
+
81
+ function deactivate() {
82
+ if (poller) poller.dispose();
83
+ if (statusBar) statusBar.dispose();
84
+ if (treeView) treeView.dispose();
85
+ if (webviewProvider) webviewProvider.dispose();
86
+ if (dashMgr) dashMgr.dispose();
87
+ }
88
+
89
+ module.exports = { activate, deactivate };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const http = require('http');
6
+
7
+ const CONFIG_FILE = '.cursor-guard.json';
8
+
9
+ class DashboardManager {
10
+ constructor() {
11
+ this._instance = null;
12
+ this._serverModule = null;
13
+ }
14
+
15
+ get running() { return !!this._instance; }
16
+ get port() { return this._instance?.port; }
17
+ get token() { return this._instance?.token; }
18
+ get baseUrl() { return this._instance ? `http://127.0.0.1:${this._instance.port}` : null; }
19
+ get registry() { return this._instance?.registry; }
20
+
21
+ async autoStart(workspaceFolders) {
22
+ if (!workspaceFolders || workspaceFolders.length === 0) return false;
23
+ const paths = workspaceFolders
24
+ .map(f => f.uri.fsPath)
25
+ .filter(p => fs.existsSync(path.join(p, CONFIG_FILE)));
26
+ if (paths.length === 0) return false;
27
+ return this.start(paths);
28
+ }
29
+
30
+ async start(paths) {
31
+ if (!this._serverModule) {
32
+ this._serverModule = require('../../dashboard/server');
33
+ }
34
+ const { startDashboardServer, getInstance } = this._serverModule;
35
+ const existing = getInstance();
36
+ if (existing) {
37
+ await startDashboardServer(paths, { silent: true });
38
+ this._instance = getInstance();
39
+ } else {
40
+ this._instance = await startDashboardServer(paths, { port: 3120, silent: true });
41
+ }
42
+ return true;
43
+ }
44
+
45
+ async fetchApi(endpoint) {
46
+ if (!this._instance) return null;
47
+ const url = `${this.baseUrl}${endpoint}${endpoint.includes('?') ? '&' : '?'}token=${this.token}`;
48
+ return new Promise((resolve, reject) => {
49
+ http.get(url, (res) => {
50
+ let data = '';
51
+ res.on('data', chunk => data += chunk);
52
+ res.on('end', () => {
53
+ try { resolve(JSON.parse(data)); }
54
+ catch { resolve(null); }
55
+ });
56
+ }).on('error', () => resolve(null));
57
+ });
58
+ }
59
+
60
+ async getProjects() {
61
+ return this.fetchApi('/api/projects') || [];
62
+ }
63
+
64
+ async getPageData(projectId, scope) {
65
+ const scopeParam = scope ? `&scope=${scope}` : '';
66
+ return this.fetchApi(`/api/page-data?id=${projectId}${scopeParam}`);
67
+ }
68
+
69
+ async snapshotNow(projectPath) {
70
+ if (!projectPath) return;
71
+ try {
72
+ const { createGitSnapshot } = require('../../lib/core/snapshot');
73
+ const { loadConfig } = require('../../lib/utils');
74
+ const cfg = loadConfig(projectPath);
75
+ return createGitSnapshot(projectPath, cfg, { message: 'guard: manual snapshot via IDE extension' });
76
+ } catch (e) {
77
+ return { status: 'error', error: e.message };
78
+ }
79
+ }
80
+
81
+ dispose() {
82
+ this._instance = null;
83
+ }
84
+ }
85
+
86
+ module.exports = { DashboardManager };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+
5
+ const POLL_INTERVAL = 5000;
6
+
7
+ class Poller {
8
+ constructor(dashMgr) {
9
+ this._dashMgr = dashMgr;
10
+ this._timer = null;
11
+ this._listeners = [];
12
+ this._data = new Map();
13
+ }
14
+
15
+ get data() { return this._data; }
16
+
17
+ onChange(fn) {
18
+ this._listeners.push(fn);
19
+ return { dispose: () => { this._listeners = this._listeners.filter(l => l !== fn); } };
20
+ }
21
+
22
+ _emit() {
23
+ for (const fn of this._listeners) {
24
+ try { fn(this._data); } catch { /* listener error */ }
25
+ }
26
+ }
27
+
28
+ start() {
29
+ if (this._timer) return;
30
+ this._poll();
31
+ this._timer = setInterval(() => this._poll(), POLL_INTERVAL);
32
+ }
33
+
34
+ stop() {
35
+ if (this._timer) { clearInterval(this._timer); this._timer = null; }
36
+ }
37
+
38
+ async _poll() {
39
+ if (!this._dashMgr.running) return;
40
+ try {
41
+ const projects = await this._dashMgr.getProjects();
42
+ if (!Array.isArray(projects)) return;
43
+ for (const p of projects) {
44
+ const pageData = await this._dashMgr.getPageData(p.id, 'dashboard');
45
+ this._data.set(p.id, { ...p, dashboard: pageData?.dashboard || null });
46
+ }
47
+ this._emit();
48
+ } catch { /* non-critical */ }
49
+ }
50
+
51
+ async forceRefresh() {
52
+ await this._poll();
53
+ }
54
+
55
+ dispose() {
56
+ this.stop();
57
+ this._listeners = [];
58
+ }
59
+ }
60
+
61
+ module.exports = { Poller };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+
5
+ class StatusBarController {
6
+ constructor(poller) {
7
+ this._item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
8
+ this._item.command = 'cursorGuard.openDashboard';
9
+ this._item.tooltip = 'Cursor Guard — click to open dashboard';
10
+ this._setIdle();
11
+ this._item.show();
12
+
13
+ this._sub = poller.onChange(data => this._update(data));
14
+ }
15
+
16
+ _setIdle() {
17
+ this._item.text = '$(shield) Guard';
18
+ this._item.backgroundColor = undefined;
19
+ }
20
+
21
+ _update(data) {
22
+ let hasAlert = false;
23
+ let watcherRunning = false;
24
+ let alertFileCount = 0;
25
+
26
+ for (const [, p] of data) {
27
+ const d = p.dashboard;
28
+ if (!d) continue;
29
+ if (d.alerts?.active) {
30
+ hasAlert = true;
31
+ alertFileCount = d.alerts.latest?.fileCount || 0;
32
+ }
33
+ if (d.watcher?.running) watcherRunning = true;
34
+ }
35
+
36
+ if (hasAlert) {
37
+ this._item.text = `$(warning) Guard: ${alertFileCount} files!`;
38
+ this._item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
39
+ this._item.tooltip = `Cursor Guard — ALERT: ${alertFileCount} files changed rapidly`;
40
+ } else if (watcherRunning) {
41
+ this._item.text = '$(shield) Guard: OK';
42
+ this._item.backgroundColor = undefined;
43
+ this._item.tooltip = 'Cursor Guard — watcher running, no alerts';
44
+ } else {
45
+ this._item.text = '$(shield) Guard';
46
+ this._item.backgroundColor = undefined;
47
+ this._item.tooltip = 'Cursor Guard — watcher not running';
48
+ }
49
+ }
50
+
51
+ dispose() {
52
+ this._sub?.dispose();
53
+ this._item.dispose();
54
+ }
55
+ }
56
+
57
+ module.exports = { StatusBarController };
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+
5
+ class GuardTreeView {
6
+ constructor(poller, dashMgr) {
7
+ this._poller = poller;
8
+ this._dashMgr = dashMgr;
9
+ this._onDidChange = new vscode.EventEmitter();
10
+ this.onDidChangeTreeData = this._onDidChange.event;
11
+
12
+ this._treeView = vscode.window.createTreeView('cursorGuardProjects', {
13
+ treeDataProvider: this,
14
+ showCollapseAll: true,
15
+ });
16
+
17
+ this._sub = poller.onChange(() => this._onDidChange.fire());
18
+ }
19
+
20
+ refresh() { this._onDidChange.fire(); }
21
+
22
+ getTreeItem(element) { return element; }
23
+
24
+ getChildren(element) {
25
+ if (!element) return this._getRootItems();
26
+ if (element.contextValue === 'project') return this._getProjectChildren(element.projectId);
27
+ return [];
28
+ }
29
+
30
+ _getRootItems() {
31
+ const data = this._poller.data;
32
+ if (data.size === 0) {
33
+ return [new TreeItem('No projects detected', 'info', { icon: 'info' })];
34
+ }
35
+ const items = [];
36
+ for (const [id, p] of data) {
37
+ const item = new TreeItem(p.name || id, 'project', {
38
+ icon: 'folder',
39
+ description: p.pathLabel,
40
+ collapsible: vscode.TreeItemCollapsibleState.Expanded,
41
+ });
42
+ item.projectId = id;
43
+ items.push(item);
44
+ }
45
+ return items;
46
+ }
47
+
48
+ _getProjectChildren(projectId) {
49
+ const p = this._poller.data.get(projectId);
50
+ if (!p?.dashboard) return [new TreeItem('Loading...', 'loading', { icon: 'loading~spin' })];
51
+ const d = p.dashboard;
52
+ const items = [];
53
+
54
+ const watcherStatus = d.watcher?.running ? 'Running' : 'Stopped';
55
+ const watcherIcon = d.watcher?.running ? 'eye' : 'eye-closed';
56
+ const watcherItem = new TreeItem(`Watcher: ${watcherStatus}`, 'watcher', { icon: watcherIcon });
57
+ if (d.watcher?.pid) watcherItem.description = `PID ${d.watcher.pid}`;
58
+ items.push(watcherItem);
59
+
60
+ if (d.lastBackup?.git) {
61
+ const ago = this._relativeTime(d.lastBackup.git.timestamp);
62
+ items.push(new TreeItem(`Last Backup: ${ago}`, 'backup', { icon: 'git-commit' }));
63
+ }
64
+
65
+ if (d.counts) {
66
+ const gitCount = d.counts.git?.commits || 0;
67
+ const shadowCount = d.counts.shadow?.snapshots || 0;
68
+ items.push(new TreeItem(`Backups: ${gitCount} git, ${shadowCount} shadow`, 'counts', { icon: 'database' }));
69
+ }
70
+
71
+ if (d.alerts?.active) {
72
+ const a = d.alerts.latest || {};
73
+ const alertItem = new TreeItem(
74
+ `ALERT: ${a.fileCount || '?'} files in ${a.windowSeconds || '?'}s`,
75
+ 'alert',
76
+ { icon: 'warning' }
77
+ );
78
+ alertItem.description = `threshold: ${a.threshold}`;
79
+ items.push(alertItem);
80
+ }
81
+
82
+ const health = d.health?.status || 'unknown';
83
+ const healthIcon = health === 'healthy' ? 'pass' : health === 'critical' ? 'error' : 'warning';
84
+ const healthItem = new TreeItem(`Health: ${health}`, 'health', { icon: healthIcon });
85
+ if (d.health?.issues?.length > 0) {
86
+ healthItem.description = d.health.issues[0];
87
+ }
88
+ items.push(healthItem);
89
+
90
+ return items;
91
+ }
92
+
93
+ _relativeTime(ts) {
94
+ const diff = Date.now() - new Date(ts).getTime();
95
+ const sec = Math.floor(diff / 1000);
96
+ if (sec < 60) return `${sec}s ago`;
97
+ const min = Math.floor(sec / 60);
98
+ if (min < 60) return `${min}m ago`;
99
+ const hr = Math.floor(min / 60);
100
+ return `${hr}h ago`;
101
+ }
102
+
103
+ dispose() {
104
+ this._sub?.dispose();
105
+ this._treeView.dispose();
106
+ this._onDidChange.dispose();
107
+ }
108
+ }
109
+
110
+ class TreeItem extends vscode.TreeItem {
111
+ constructor(label, contextValue, opts = {}) {
112
+ super(label, opts.collapsible || vscode.TreeItemCollapsibleState.None);
113
+ this.contextValue = contextValue;
114
+ if (opts.icon) this.iconPath = new vscode.ThemeIcon(opts.icon);
115
+ if (opts.description) this.description = opts.description;
116
+ }
117
+ }
118
+
119
+ module.exports = { GuardTreeView };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const PUBLIC_DIR = path.resolve(__dirname, '..', '..', 'dashboard', 'public');
8
+
9
+ class WebViewProvider {
10
+ constructor(context, dashMgr) {
11
+ this._context = context;
12
+ this._dashMgr = dashMgr;
13
+ this._panel = null;
14
+ }
15
+
16
+ show() {
17
+ if (this._panel) {
18
+ this._panel.reveal();
19
+ return;
20
+ }
21
+
22
+ this._panel = vscode.window.createWebviewPanel(
23
+ 'cursorGuardDashboard',
24
+ 'Cursor Guard Dashboard',
25
+ vscode.ViewColumn.One,
26
+ {
27
+ enableScripts: true,
28
+ retainContextWhenHidden: true,
29
+ localResourceRoots: [vscode.Uri.file(PUBLIC_DIR)],
30
+ }
31
+ );
32
+
33
+ this._panel.webview.html = this._buildHtml(this._panel.webview);
34
+ this._panel.iconPath = new vscode.ThemeIcon('shield');
35
+
36
+ this._panel.onDidDispose(() => { this._panel = null; });
37
+
38
+ this._panel.webview.onDidReceiveMessage(msg => {
39
+ if (msg.type === 'copy') {
40
+ vscode.env.clipboard.writeText(msg.text);
41
+ vscode.window.showInformationMessage('Copied to clipboard');
42
+ }
43
+ });
44
+ }
45
+
46
+ _buildHtml(webview) {
47
+ const htmlPath = path.join(PUBLIC_DIR, 'index.html');
48
+ let html = fs.readFileSync(htmlPath, 'utf-8');
49
+
50
+ const styleUri = webview.asWebviewUri(vscode.Uri.file(path.join(PUBLIC_DIR, 'style.css')));
51
+ const scriptUri = webview.asWebviewUri(vscode.Uri.file(path.join(PUBLIC_DIR, 'app.js')));
52
+
53
+ html = html.replace(/href="style\.css"/g, `href="${styleUri}"`);
54
+ html = html.replace(/src="app\.js"/g, `src="${scriptUri}"`);
55
+
56
+ const baseUrl = this._dashMgr.baseUrl || '';
57
+ const token = this._dashMgr.token || '';
58
+ const nonce = _getNonce();
59
+
60
+ html = html.replace(
61
+ '</head>',
62
+ `<script nonce="${nonce}">
63
+ window.__GUARD_TOKEN__ = "${token}";
64
+ window.__GUARD_BASE_URL__ = "${baseUrl}";
65
+ window.__IN_VSCODE__ = true;
66
+ </script>
67
+ </head>`
68
+ );
69
+
70
+ return html;
71
+ }
72
+
73
+ dispose() {
74
+ if (this._panel) { this._panel.dispose(); this._panel = null; }
75
+ }
76
+ }
77
+
78
+ function _getNonce() {
79
+ let text = '';
80
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
81
+ for (let i = 0; i < 32; i++) text += chars.charAt(Math.floor(Math.random() * chars.length));
82
+ return text;
83
+ }
84
+
85
+ module.exports = { WebViewProvider };
@@ -0,0 +1,5 @@
1
+ ## Extension Icon
2
+
3
+ Place a 128x128 PNG file named `icon.png` in this directory for the VS Code marketplace listing.
4
+
5
+ The `guard-icon.svg` is used for the activity bar sidebar icon.
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
3
+ <path d="M9 12l2 2 4-4"/>
4
+ </svg>
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "cursor-guard-ide",
3
+ "displayName": "Cursor Guard",
4
+ "description": "AI code protection dashboard embedded in your IDE — real-time alerts, backup history, one-click snapshots",
5
+ "version": "4.7.0",
6
+ "publisher": "zhangqiang8vipp",
7
+ "license": "BUSL-1.1",
8
+ "engines": {
9
+ "vscode": "^1.74.0"
10
+ },
11
+ "categories": ["Other", "Visualization"],
12
+ "keywords": ["cursor", "ai-safety", "code-protection", "git-backup", "dashboard"],
13
+ "icon": "media/icon.png",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/zhangqiang8vipp/cursor-guard"
17
+ },
18
+ "main": "./extension.js",
19
+ "activationEvents": [
20
+ "workspaceContains:.cursor-guard.json"
21
+ ],
22
+ "contributes": {
23
+ "commands": [
24
+ {
25
+ "command": "cursorGuard.openDashboard",
26
+ "title": "Open Dashboard",
27
+ "category": "Cursor Guard",
28
+ "icon": "$(dashboard)"
29
+ },
30
+ {
31
+ "command": "cursorGuard.snapshotNow",
32
+ "title": "Snapshot Now",
33
+ "category": "Cursor Guard",
34
+ "icon": "$(device-camera)"
35
+ },
36
+ {
37
+ "command": "cursorGuard.startWatcher",
38
+ "title": "Start Watcher",
39
+ "category": "Cursor Guard",
40
+ "icon": "$(eye)"
41
+ },
42
+ {
43
+ "command": "cursorGuard.stopWatcher",
44
+ "title": "Stop Watcher",
45
+ "category": "Cursor Guard",
46
+ "icon": "$(eye-closed)"
47
+ },
48
+ {
49
+ "command": "cursorGuard.refreshTree",
50
+ "title": "Refresh",
51
+ "category": "Cursor Guard",
52
+ "icon": "$(refresh)"
53
+ }
54
+ ],
55
+ "viewsContainers": {
56
+ "activitybar": [
57
+ {
58
+ "id": "cursorGuard",
59
+ "title": "Cursor Guard",
60
+ "icon": "media/guard-icon.svg"
61
+ }
62
+ ]
63
+ },
64
+ "views": {
65
+ "cursorGuard": [
66
+ {
67
+ "id": "cursorGuardProjects",
68
+ "name": "Projects"
69
+ }
70
+ ]
71
+ },
72
+ "menus": {
73
+ "view/title": [
74
+ {
75
+ "command": "cursorGuard.openDashboard",
76
+ "when": "view == cursorGuardProjects",
77
+ "group": "navigation"
78
+ },
79
+ {
80
+ "command": "cursorGuard.refreshTree",
81
+ "when": "view == cursorGuardProjects",
82
+ "group": "navigation"
83
+ }
84
+ ]
85
+ }
86
+ }
87
+ }