clawarmor 2.1.0 → 2.2.1

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.
@@ -0,0 +1,104 @@
1
+ // clawarmor snapshot — Config snapshot save/load/list/restore.
2
+ // Snapshots are saved to ~/.clawarmor/snapshots/<timestamp>.json.
3
+ // Max 20 snapshots are kept; oldest are pruned on new save.
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, chmodSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { homedir } from 'os';
8
+
9
+ const HOME = homedir();
10
+ const SNAPSHOTS_DIR = join(HOME, '.clawarmor', 'snapshots');
11
+ const MAX_SNAPSHOTS = 20;
12
+
13
+ /** @returns {string[]} snapshot filenames sorted oldest-first */
14
+ function listSnapshotFiles() {
15
+ if (!existsSync(SNAPSHOTS_DIR)) return [];
16
+ try {
17
+ return readdirSync(SNAPSHOTS_DIR).filter(f => f.endsWith('.json')).sort();
18
+ } catch { return []; }
19
+ }
20
+
21
+ /** Prune to MAX_SNAPSHOTS, removing oldest entries. */
22
+ function pruneSnapshots() {
23
+ const files = listSnapshotFiles();
24
+ if (files.length <= MAX_SNAPSHOTS) return;
25
+ for (const f of files.slice(0, files.length - MAX_SNAPSHOTS)) {
26
+ try { unlinkSync(join(SNAPSHOTS_DIR, f)); } catch { /* non-fatal */ }
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Save a snapshot before applying fixes.
32
+ * @param {{ trigger: string, configPath: string, configContent: string|null, filePermissions: Object, appliedFixes: string[] }} opts
33
+ * @returns {string|null} snapshot id or null on failure
34
+ */
35
+ export function saveSnapshot({ trigger, configPath, configContent, filePermissions = {}, appliedFixes = [] }) {
36
+ try {
37
+ if (!existsSync(SNAPSHOTS_DIR)) mkdirSync(SNAPSHOTS_DIR, { recursive: true });
38
+ const timestamp = new Date().toISOString();
39
+ const id = timestamp.replace(/[:.]/g, '-');
40
+ const snapshot = { timestamp, trigger, configPath, configContent, filePermissions, appliedFixes };
41
+ writeFileSync(join(SNAPSHOTS_DIR, `${id}.json`), JSON.stringify(snapshot, null, 2), 'utf8');
42
+ pruneSnapshots();
43
+ return id;
44
+ } catch { return null; }
45
+ }
46
+
47
+ /**
48
+ * List all snapshots, newest first.
49
+ * @returns {Array<{ id: string, timestamp: string, trigger: string, appliedFixes: string[], configPath: string }>}
50
+ */
51
+ export function listSnapshots() {
52
+ return listSnapshotFiles().reverse().map(f => {
53
+ try {
54
+ const data = JSON.parse(readFileSync(join(SNAPSHOTS_DIR, f), 'utf8'));
55
+ return {
56
+ id: f.replace('.json', ''),
57
+ timestamp: data.timestamp,
58
+ trigger: data.trigger || 'unknown',
59
+ appliedFixes: data.appliedFixes || [],
60
+ configPath: data.configPath || null,
61
+ };
62
+ } catch { return null; }
63
+ }).filter(Boolean);
64
+ }
65
+
66
+ /**
67
+ * Load a specific snapshot by id.
68
+ * @param {string} id
69
+ * @returns {Object|null}
70
+ */
71
+ export function loadSnapshot(id) {
72
+ const filePath = join(SNAPSHOTS_DIR, `${id}.json`);
73
+ if (!existsSync(filePath)) return null;
74
+ try { return JSON.parse(readFileSync(filePath, 'utf8')); } catch { return null; }
75
+ }
76
+
77
+ /**
78
+ * Load the most recent snapshot.
79
+ * @returns {Object|null}
80
+ */
81
+ export function loadLatestSnapshot() {
82
+ const files = listSnapshotFiles();
83
+ if (!files.length) return null;
84
+ try { return JSON.parse(readFileSync(join(SNAPSHOTS_DIR, files[files.length - 1]), 'utf8')); } catch { return null; }
85
+ }
86
+
87
+ /**
88
+ * Restore a snapshot: writes config content and restores file permissions.
89
+ * @param {Object} snapshot
90
+ * @returns {{ ok: boolean, err?: string }}
91
+ */
92
+ export function restoreSnapshot(snapshot) {
93
+ try {
94
+ if (snapshot.configPath && snapshot.configContent != null) {
95
+ writeFileSync(snapshot.configPath, snapshot.configContent, 'utf8');
96
+ }
97
+ for (const [filePath, octalStr] of Object.entries(snapshot.filePermissions || {})) {
98
+ try {
99
+ if (existsSync(filePath)) chmodSync(filePath, parseInt(octalStr, 8));
100
+ } catch { /* non-fatal per-file */ }
101
+ }
102
+ return { ok: true };
103
+ } catch (e) { return { ok: false, err: e.message }; }
104
+ }
package/lib/status.js CHANGED
@@ -7,6 +7,7 @@ import { homedir } from 'os';
7
7
  import { paint } from './output/colors.js';
8
8
  import { scoreToGrade, scoreColor, gradeColor } from './output/progress.js';
9
9
  import { watchDaemonStatus } from './watch.js';
10
+ import { getMonitorStatus } from './monitor.js';
10
11
 
11
12
  const HOME = homedir();
12
13
  const OC_DIR = join(HOME, '.openclaw');
@@ -20,7 +21,7 @@ const FISH_FUNCTION_FILE = join(HOME, '.config', 'fish', 'functions', 'openclaw.
20
21
  const SHELL_MARKER = '# ClawArmor intercept — added by: clawarmor protect --install';
21
22
  const CRON_JOBS_FILE = join(OC_DIR, 'cron', 'jobs.json');
22
23
  const HOOKS_DIR = join(OC_DIR, 'hooks', 'clawarmor-guard');
23
- const VERSION = '2.0.0';
24
+ const VERSION = '2.2.0';
24
25
 
25
26
  const SEP = paint.dim('─'.repeat(52));
26
27
 
@@ -212,6 +213,14 @@ export async function runStatus() {
212
213
  }
213
214
  console.log(` ${paint.dim('Watcher')} ${watchStr}`);
214
215
 
216
+ // ── Monitor mode ──────────────────────────────────────────────────────────
217
+ const monitorStatus = getMonitorStatus();
218
+ if (monitorStatus.enabled) {
219
+ const startedAgo = timeAgo(monitorStatus.startedAt);
220
+ const fixCount = monitorStatus.fixes?.length || 0;
221
+ console.log(` ${paint.dim('Monitor')} ${paint.yellow('●')} ${paint.bold('active')} ${paint.dim('(started ' + startedAgo + ', ' + fixCount + ' fix' + (fixCount !== 1 ? 'es' : '') + ')')} ${paint.dim('→ clawarmor harden --monitor-report')}`);
222
+ }
223
+
215
224
  // ── Shell intercept ───────────────────────────────────────────────────────
216
225
  const icp = intercept();
217
226
  const icpStr = icp.active
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clawarmor",
3
- "version": "2.1.0",
4
- "description": "Security armor for OpenClaw agents \u2014 audit, scan, monitor",
3
+ "version": "2.2.1",
4
+ "description": "Security armor for OpenClaw agents audit, scan, monitor",
5
5
  "bin": {
6
6
  "clawarmor": "cli.js"
7
7
  },
package/demo-preview.gif DELETED
Binary file